rewrite parsing, evaluation, and serialization (#67)
Adds support for the indented syntax, plain CSS imports, `@forward`, and many other previously missing features.
This commit is contained in:
parent
b913eabdf1
commit
ffaee04613
@ -1,19 +1,20 @@
|
||||
---
|
||||
name: Incorrect SASS Output
|
||||
about: There exists a differential between the output of grass and dart-sass
|
||||
name: Incorrect Sass Output
|
||||
about: `grass` and `dart-sass` differ in output or `grass` reports and error for a valid style sheet
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: connorskees
|
||||
|
||||
---
|
||||
|
||||
**Minimal Reproducible Example**:
|
||||
**Failing Sass**:
|
||||
```
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Showing output from both tools is optional, but does help in debugging -->
|
||||
**`grass` Output**:
|
||||
```
|
||||
a {
|
||||
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
52
CHANGELOG.md
52
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
|
||||
|
49
Cargo.toml
49
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<path>
|
||||
# todo: update to expose more info (for eww)
|
||||
# todo: update to use text_size::TextRange
|
||||
codemap = "0.1.3"
|
||||
wasm-bindgen = { version = "0.2.68", optional = true }
|
||||
beef = "0.5"
|
||||
# todo: use phf for global functions
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
# criterion is not a dev-dependency because it makes tests take too
|
||||
# long to compile, and you cannot make dev-dependencies optional
|
||||
criterion = { version = "0.4.0", optional = true }
|
||||
indexmap = "1.9.1"
|
||||
indexmap = "1.9.0"
|
||||
# todo: do we really need interning for things?
|
||||
lasso = "0.6"
|
||||
|
||||
[features]
|
||||
# todo: no commandline by default
|
||||
default = ["commandline", "random"]
|
||||
# Option (enabled by default): build a binary using clap
|
||||
commandline = ["clap"]
|
||||
# Option: enable nightly-only features (for right now, only the `track_caller` attribute)
|
||||
nightly = []
|
||||
# Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()`
|
||||
random = ["rand"]
|
||||
# Option: expose JavaScript-friendly WebAssembly exports
|
||||
wasm-exports = ["wasm-bindgen"]
|
||||
# Option: enable features that assist in profiling (e.g. inline(never))
|
||||
profiling = []
|
||||
# Option: enable criterion for benchmarking
|
||||
bench = ["criterion"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
|
48
README.md
48
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
|
||||
```
|
||||
|
||||
<!-- todo: msrv 1.41.1 -->
|
@ -1,5 +0,0 @@
|
||||
@for $i from 0 to 250 {
|
||||
a {
|
||||
color: $i;
|
||||
}
|
||||
}
|
@ -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);
|
@ -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);
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,502 +0,0 @@
|
||||
a {
|
||||
color: mediumvioletred;
|
||||
color: burlywood;
|
||||
color: deeppink;
|
||||
color: lavenderblush;
|
||||
color: steelblue;
|
||||
color: lightslategray;
|
||||
color: palevioletred;
|
||||
color: rosybrown;
|
||||
color: whitesmoke;
|
||||
color: navy;
|
||||
color: blue;
|
||||
color: darkolivegreen;
|
||||
color: transparent;
|
||||
color: black;
|
||||
color: lightskyblue;
|
||||
color: sandybrown;
|
||||
color: darkturquoise;
|
||||
color: darkorange;
|
||||
color: tan;
|
||||
color: tomato;
|
||||
color: lightgray;
|
||||
color: seagreen;
|
||||
color: cadetblue;
|
||||
color: crimson;
|
||||
color: darksalmon;
|
||||
color: mediumpurple;
|
||||
color: mistyrose;
|
||||
color: cornflowerblue;
|
||||
color: gray;
|
||||
color: lightyellow;
|
||||
color: purple;
|
||||
color: darkred;
|
||||
color: mediumturquoise;
|
||||
color: rosybrown;
|
||||
color: sandybrown;
|
||||
color: mediumblue;
|
||||
color: darkgoldenrod;
|
||||
color: lightgreen;
|
||||
color: aquamarine;
|
||||
color: linen;
|
||||
color: pink;
|
||||
color: oldlace;
|
||||
color: lightgoldenrodyellow;
|
||||
color: chocolate;
|
||||
color: lightblue;
|
||||
color: mediumseagreen;
|
||||
color: honeydew;
|
||||
color: powderblue;
|
||||
color: floralwhite;
|
||||
color: royalblue;
|
||||
color: greenyellow;
|
||||
color: lightyellow;
|
||||
color: beige;
|
||||
color: thistle;
|
||||
color: dodgerblue;
|
||||
color: navajowhite;
|
||||
color: lightseagreen;
|
||||
color: saddlebrown;
|
||||
color: moccasin;
|
||||
color: turquoise;
|
||||
color: purple;
|
||||
color: darkgray;
|
||||
color: thistle;
|
||||
color: mistyrose;
|
||||
color: salmon;
|
||||
color: palegoldenrod;
|
||||
color: white;
|
||||
color: cornflowerblue;
|
||||
color: lightyellow;
|
||||
color: snow;
|
||||
color: aqua;
|
||||
color: indianred;
|
||||
color: lightyellow;
|
||||
color: darkkhaki;
|
||||
color: aqua;
|
||||
color: darkviolet;
|
||||
color: powderblue;
|
||||
color: darkblue;
|
||||
color: papayawhip;
|
||||
color: hotpink;
|
||||
color: chocolate;
|
||||
color: mediumaquamarine;
|
||||
color: lightskyblue;
|
||||
color: mediumvioletred;
|
||||
color: white;
|
||||
color: lightgreen;
|
||||
color: palevioletred;
|
||||
color: antiquewhite;
|
||||
color: indianred;
|
||||
color: darkgreen;
|
||||
color: darkmagenta;
|
||||
color: darkviolet;
|
||||
color: snow;
|
||||
color: lightgoldenrodyellow;
|
||||
color: darksalmon;
|
||||
color: royalblue;
|
||||
color: cornsilk;
|
||||
color: deepskyblue;
|
||||
color: lightseagreen;
|
||||
color: skyblue;
|
||||
color: mediumblue;
|
||||
color: azure;
|
||||
color: firebrick;
|
||||
color: turquoise;
|
||||
color: plum;
|
||||
color: aqua;
|
||||
color: chocolate;
|
||||
color: lightyellow;
|
||||
color: coral;
|
||||
color: darkseagreen;
|
||||
color: antiquewhite;
|
||||
color: cornflowerblue;
|
||||
color: chartreuse;
|
||||
color: darkcyan;
|
||||
color: snow;
|
||||
color: honeydew;
|
||||
color: tomato;
|
||||
color: darkturquoise;
|
||||
color: papayawhip;
|
||||
color: lightskyblue;
|
||||
color: honeydew;
|
||||
color: cornflowerblue;
|
||||
color: darkgray;
|
||||
color: mediumseagreen;
|
||||
color: thistle;
|
||||
color: darkgoldenrod;
|
||||
color: forestgreen;
|
||||
color: black;
|
||||
color: cornflowerblue;
|
||||
color: blanchedalmond;
|
||||
color: aliceblue;
|
||||
color: mediumblue;
|
||||
color: blueviolet;
|
||||
color: coral;
|
||||
color: dodgerblue;
|
||||
color: whitesmoke;
|
||||
color: yellow;
|
||||
color: burlywood;
|
||||
color: whitesmoke;
|
||||
color: bisque;
|
||||
color: palegreen;
|
||||
color: darkblue;
|
||||
color: fuchsia;
|
||||
color: darkviolet;
|
||||
color: orangered;
|
||||
color: thistle;
|
||||
color: darkkhaki;
|
||||
color: mediumpurple;
|
||||
color: lightslategray;
|
||||
color: wheat;
|
||||
color: brown;
|
||||
color: oldlace;
|
||||
color: mintcream;
|
||||
color: ivory;
|
||||
color: gold;
|
||||
color: forestgreen;
|
||||
color: black;
|
||||
color: darkorchid;
|
||||
color: springgreen;
|
||||
color: mediumvioletred;
|
||||
color: navajowhite;
|
||||
color: aquamarine;
|
||||
color: crimson;
|
||||
color: dodgerblue;
|
||||
color: slateblue;
|
||||
color: lawngreen;
|
||||
color: lightgray;
|
||||
color: peachpuff;
|
||||
color: lightgreen;
|
||||
color: yellow;
|
||||
color: gold;
|
||||
color: silver;
|
||||
color: lightblue;
|
||||
color: bisque;
|
||||
color: mediumorchid;
|
||||
color: violet;
|
||||
color: darkturquoise;
|
||||
color: steelblue;
|
||||
color: black;
|
||||
color: palegoldenrod;
|
||||
color: gray;
|
||||
color: khaki;
|
||||
color: linen;
|
||||
color: purple;
|
||||
color: skyblue;
|
||||
color: beige;
|
||||
color: ghostwhite;
|
||||
color: saddlebrown;
|
||||
color: yellow;
|
||||
color: dimgray;
|
||||
color: floralwhite;
|
||||
color: lightgray;
|
||||
color: powderblue;
|
||||
color: aquamarine;
|
||||
color: black;
|
||||
color: lightgray;
|
||||
color: olive;
|
||||
color: darkkhaki;
|
||||
color: darkmagenta;
|
||||
color: darkturquoise;
|
||||
color: ghostwhite;
|
||||
color: turquoise;
|
||||
color: blue;
|
||||
color: darkorange;
|
||||
color: oldlace;
|
||||
color: saddlebrown;
|
||||
color: lightcoral;
|
||||
color: fuchsia;
|
||||
color: olivedrab;
|
||||
color: seagreen;
|
||||
color: dodgerblue;
|
||||
color: ghostwhite;
|
||||
color: antiquewhite;
|
||||
color: indianred;
|
||||
color: honeydew;
|
||||
color: antiquewhite;
|
||||
color: darkorchid;
|
||||
color: gainsboro;
|
||||
color: whitesmoke;
|
||||
color: hotpink;
|
||||
color: indianred;
|
||||
color: lightgoldenrodyellow;
|
||||
color: mintcream;
|
||||
color: peachpuff;
|
||||
color: goldenrod;
|
||||
color: orangered;
|
||||
color: skyblue;
|
||||
color: plum;
|
||||
color: slateblue;
|
||||
color: mediumslateblue;
|
||||
color: olivedrab;
|
||||
color: indigo;
|
||||
color: lightgoldenrodyellow;
|
||||
color: red;
|
||||
color: lemonchiffon;
|
||||
color: bisque;
|
||||
color: crimson;
|
||||
color: cadetblue;
|
||||
color: mediumblue;
|
||||
color: orange;
|
||||
color: darkslateblue;
|
||||
color: olivedrab;
|
||||
color: violet;
|
||||
color: mediumspringgreen;
|
||||
color: indigo;
|
||||
color: moccasin;
|
||||
color: lightpink;
|
||||
color: deepskyblue;
|
||||
color: oldlace;
|
||||
color: lightsalmon;
|
||||
color: mediumturquoise;
|
||||
color: darksalmon;
|
||||
color: darkblue;
|
||||
color: dimgray;
|
||||
color: blanchedalmond;
|
||||
color: mediumturquoise;
|
||||
color: black;
|
||||
color: peachpuff;
|
||||
color: olivedrab;
|
||||
color: darkgreen;
|
||||
color: white;
|
||||
color: paleturquoise;
|
||||
color: aliceblue;
|
||||
color: limegreen;
|
||||
color: darkslateblue;
|
||||
color: skyblue;
|
||||
color: darksalmon;
|
||||
color: salmon;
|
||||
color: darkcyan;
|
||||
color: pink;
|
||||
color: saddlebrown;
|
||||
color: blue;
|
||||
color: blue;
|
||||
color: papayawhip;
|
||||
color: mediumvioletred;
|
||||
color: darksalmon;
|
||||
color: darkolivegreen;
|
||||
color: yellowgreen;
|
||||
color: wheat;
|
||||
color: darkslategray;
|
||||
color: purple;
|
||||
color: red;
|
||||
color: mistyrose;
|
||||
color: palegreen;
|
||||
color: cornflowerblue;
|
||||
color: seashell;
|
||||
color: mediumpurple;
|
||||
color: darkslateblue;
|
||||
color: honeydew;
|
||||
color: chocolate;
|
||||
color: ivory;
|
||||
color: mediumslateblue;
|
||||
color: darkturquoise;
|
||||
color: navajowhite;
|
||||
color: red;
|
||||
color: sienna;
|
||||
color: gray;
|
||||
color: cadetblue;
|
||||
color: silver;
|
||||
color: burlywood;
|
||||
color: cornflowerblue;
|
||||
color: palegoldenrod;
|
||||
color: yellow;
|
||||
color: chocolate;
|
||||
color: darkseagreen;
|
||||
color: lightpink;
|
||||
color: chocolate;
|
||||
color: tomato;
|
||||
color: thistle;
|
||||
color: tomato;
|
||||
color: whitesmoke;
|
||||
color: indianred;
|
||||
color: lightgreen;
|
||||
color: peru;
|
||||
color: orange;
|
||||
color: palegoldenrod;
|
||||
color: darkkhaki;
|
||||
color: olive;
|
||||
color: chocolate;
|
||||
color: gainsboro;
|
||||
color: chocolate;
|
||||
color: oldlace;
|
||||
color: royalblue;
|
||||
color: dodgerblue;
|
||||
color: darkmagenta;
|
||||
color: saddlebrown;
|
||||
color: beige;
|
||||
color: floralwhite;
|
||||
color: aliceblue;
|
||||
color: aquamarine;
|
||||
color: mintcream;
|
||||
color: mintcream;
|
||||
color: palegreen;
|
||||
color: yellow;
|
||||
color: lightsteelblue;
|
||||
color: salmon;
|
||||
color: darkviolet;
|
||||
color: whitesmoke;
|
||||
color: salmon;
|
||||
color: violet;
|
||||
color: aliceblue;
|
||||
color: mediumspringgreen;
|
||||
color: firebrick;
|
||||
color: goldenrod;
|
||||
color: gold;
|
||||
color: honeydew;
|
||||
color: lawngreen;
|
||||
color: azure;
|
||||
color: ghostwhite;
|
||||
color: lightsalmon;
|
||||
color: oldlace;
|
||||
color: lime;
|
||||
color: indigo;
|
||||
color: saddlebrown;
|
||||
color: mediumaquamarine;
|
||||
color: rosybrown;
|
||||
color: gray;
|
||||
color: seashell;
|
||||
color: midnightblue;
|
||||
color: slateblue;
|
||||
color: snow;
|
||||
color: wheat;
|
||||
color: indigo;
|
||||
color: tomato;
|
||||
color: lightyellow;
|
||||
color: cornflowerblue;
|
||||
color: lightgray;
|
||||
color: slategray;
|
||||
color: steelblue;
|
||||
color: skyblue;
|
||||
color: oldlace;
|
||||
color: darkseagreen;
|
||||
color: lawngreen;
|
||||
color: gainsboro;
|
||||
color: aquamarine;
|
||||
color: snow;
|
||||
color: royalblue;
|
||||
color: dimgray;
|
||||
color: orangered;
|
||||
color: forestgreen;
|
||||
color: honeydew;
|
||||
color: darksalmon;
|
||||
color: chartreuse;
|
||||
color: mediumblue;
|
||||
color: mediumpurple;
|
||||
color: lightyellow;
|
||||
color: deeppink;
|
||||
color: darkgreen;
|
||||
color: peachpuff;
|
||||
color: mintcream;
|
||||
color: mediumblue;
|
||||
color: sandybrown;
|
||||
color: green;
|
||||
color: darkolivegreen;
|
||||
color: crimson;
|
||||
color: darkslateblue;
|
||||
color: rosybrown;
|
||||
color: blueviolet;
|
||||
color: darkgray;
|
||||
color: transparent;
|
||||
color: darkslategray;
|
||||
color: lightcyan;
|
||||
color: honeydew;
|
||||
color: teal;
|
||||
color: brown;
|
||||
color: darkorchid;
|
||||
color: fuchsia;
|
||||
color: lime;
|
||||
color: mediumpurple;
|
||||
color: darkorange;
|
||||
color: midnightblue;
|
||||
color: mediumvioletred;
|
||||
color: limegreen;
|
||||
color: lightseagreen;
|
||||
color: mistyrose;
|
||||
color: burlywood;
|
||||
color: wheat;
|
||||
color: maroon;
|
||||
color: darkgoldenrod;
|
||||
color: hotpink;
|
||||
color: lightskyblue;
|
||||
color: darkgreen;
|
||||
color: yellowgreen;
|
||||
color: mintcream;
|
||||
color: navy;
|
||||
color: oldlace;
|
||||
color: papayawhip;
|
||||
color: powderblue;
|
||||
color: lightskyblue;
|
||||
color: lightyellow;
|
||||
color: yellowgreen;
|
||||
color: deepskyblue;
|
||||
color: purple;
|
||||
color: lemonchiffon;
|
||||
color: darkgoldenrod;
|
||||
color: lightskyblue;
|
||||
color: salmon;
|
||||
color: snow;
|
||||
color: darkgoldenrod;
|
||||
color: azure;
|
||||
color: lightpink;
|
||||
color: bisque;
|
||||
color: palegreen;
|
||||
color: darkviolet;
|
||||
color: slateblue;
|
||||
color: blue;
|
||||
color: orchid;
|
||||
color: ghostwhite;
|
||||
color: lavender;
|
||||
color: lavenderblush;
|
||||
color: cornsilk;
|
||||
color: teal;
|
||||
color: lightcyan;
|
||||
color: darkslategray;
|
||||
color: powderblue;
|
||||
color: lightyellow;
|
||||
color: powderblue;
|
||||
color: bisque;
|
||||
color: tomato;
|
||||
color: ghostwhite;
|
||||
color: papayawhip;
|
||||
color: thistle;
|
||||
color: firebrick;
|
||||
color: mediumspringgreen;
|
||||
color: darkkhaki;
|
||||
color: indigo;
|
||||
color: azure;
|
||||
color: chartreuse;
|
||||
color: whitesmoke;
|
||||
color: forestgreen;
|
||||
color: darkorange;
|
||||
color: darkslategray;
|
||||
color: honeydew;
|
||||
color: dodgerblue;
|
||||
color: skyblue;
|
||||
color: mediumspringgreen;
|
||||
color: olivedrab;
|
||||
color: greenyellow;
|
||||
color: wheat;
|
||||
color: seagreen;
|
||||
color: crimson;
|
||||
color: lavender;
|
||||
color: steelblue;
|
||||
color: aliceblue;
|
||||
color: chartreuse;
|
||||
color: orangered;
|
||||
color: transparent;
|
||||
color: dimgray;
|
||||
color: palegreen;
|
||||
color: forestgreen;
|
||||
color: mediumorchid;
|
||||
color: darkorchid;
|
||||
color: pink;
|
||||
color: aliceblue;
|
||||
color: greenyellow;
|
||||
color: darkslategray;
|
||||
color: paleturquoise;
|
||||
color: lightslategray;
|
||||
color: darkturquoise;
|
||||
color: sandybrown;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -1,3 +1,3 @@
|
||||
body {
|
||||
background: red;
|
||||
a {
|
||||
color: red;
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit e348959657f1e274cef658283436a311a925a673
|
||||
Subproject commit f7265276e53b0c5e6df0f800ed4b0ae61fbd0351
|
237
src/args.rs
237
src/args.rs
@ -1,237 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
value::Value,
|
||||
{Cow, Token},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct FuncArgs(pub Vec<FuncArg>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct FuncArg {
|
||||
pub name: Identifier,
|
||||
pub default: Option<Vec<Token>>,
|
||||
pub is_variadic: bool,
|
||||
}
|
||||
|
||||
impl FuncArgs {
|
||||
pub const fn new() -> Self {
|
||||
FuncArgs(Vec::new())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CallArgs(pub HashMap<CallArg, SassResult<Spanned<Value>>>, pub Span);
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub(crate) enum CallArg {
|
||||
Named(Identifier),
|
||||
Positional(usize),
|
||||
}
|
||||
|
||||
impl CallArg {
|
||||
pub fn position(&self) -> Result<usize, String> {
|
||||
match self {
|
||||
Self::Named(ref name) => Err(name.to_string()),
|
||||
Self::Positional(p) => Ok(*p),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement(self) -> CallArg {
|
||||
match self {
|
||||
Self::Named(..) => self,
|
||||
Self::Positional(p) => Self::Positional(p - 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallArgs {
|
||||
pub fn new(span: Span) -> Self {
|
||||
CallArgs(HashMap::new(), span)
|
||||
}
|
||||
|
||||
pub fn to_css_string(self, is_compressed: bool) -> SassResult<Spanned<String>> {
|
||||
let mut string = String::with_capacity(2 + self.len() * 10);
|
||||
string.push('(');
|
||||
let mut span = self.1;
|
||||
|
||||
if self.is_empty() {
|
||||
return Ok(Spanned {
|
||||
node: "()".to_owned(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
|
||||
let args = match self.get_variadic() {
|
||||
Ok(v) => v,
|
||||
Err(..) => {
|
||||
return Err(("Plain CSS functions don't support keyword arguments.", span).into())
|
||||
}
|
||||
};
|
||||
|
||||
string.push_str(
|
||||
&args
|
||||
.iter()
|
||||
.map(|a| {
|
||||
span = span.merge(a.span);
|
||||
a.node.to_css_string(a.span, is_compressed)
|
||||
})
|
||||
.collect::<SassResult<Vec<Cow<'static, str>>>>()?
|
||||
.join(", "),
|
||||
);
|
||||
string.push(')');
|
||||
Ok(Spanned { node: string, span })
|
||||
}
|
||||
|
||||
/// Get argument by name
|
||||
///
|
||||
/// Removes the argument
|
||||
pub fn get_named<T: Into<Identifier>>(&mut self, val: T) -> Option<SassResult<Spanned<Value>>> {
|
||||
self.0.remove(&CallArg::Named(val.into()))
|
||||
}
|
||||
|
||||
/// Get a positional argument by 0-indexed position
|
||||
///
|
||||
/// Removes the argument
|
||||
pub fn get_positional(&mut self, val: usize) -> Option<SassResult<Spanned<Value>>> {
|
||||
self.0.remove(&CallArg::Positional(val))
|
||||
}
|
||||
|
||||
pub fn get<T: Into<Identifier>>(
|
||||
&mut self,
|
||||
position: usize,
|
||||
name: T,
|
||||
) -> Option<SassResult<Spanned<Value>>> {
|
||||
match self.get_named(name) {
|
||||
Some(v) => Some(v),
|
||||
None => self.get_positional(position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_err(&mut self, position: usize, name: &'static str) -> SassResult<Value> {
|
||||
match self.get_named(name) {
|
||||
Some(v) => Ok(v?.node),
|
||||
None => match self.get_positional(position) {
|
||||
Some(v) => Ok(v?.node),
|
||||
None => Err((format!("Missing argument ${}.", name), self.span()).into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement all positional arguments by 1
|
||||
///
|
||||
/// This is used by builtin function `call` to pass
|
||||
/// positional arguments to the other function
|
||||
pub fn decrement(self) -> Self {
|
||||
CallArgs(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.decrement(), v))
|
||||
.collect(),
|
||||
self.1,
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn span(&self) -> Span {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn min_args(&self, min: usize) -> SassResult<()> {
|
||||
let len = self.len();
|
||||
if len < min {
|
||||
if min == 1 {
|
||||
return Err(("At least one argument must be passed.", self.span()).into());
|
||||
}
|
||||
todo!("min args greater than one")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn max_args(&self, max: usize) -> SassResult<()> {
|
||||
let len = self.len();
|
||||
if len > max {
|
||||
let mut err = String::with_capacity(50);
|
||||
#[allow(clippy::format_push_string)]
|
||||
err.push_str(&format!("Only {} argument", max));
|
||||
if max != 1 {
|
||||
err.push('s');
|
||||
}
|
||||
err.push_str(" allowed, but ");
|
||||
err.push_str(&len.to_string());
|
||||
err.push(' ');
|
||||
if len == 1 {
|
||||
err.push_str("was passed.");
|
||||
} else {
|
||||
err.push_str("were passed.");
|
||||
}
|
||||
return Err((err, self.span()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_arg(
|
||||
&mut self,
|
||||
position: usize,
|
||||
name: &'static str,
|
||||
default: Value,
|
||||
) -> SassResult<Value> {
|
||||
Ok(match self.get(position, name) {
|
||||
Some(val) => val?.node,
|
||||
None => default,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn positional_arg(&mut self, position: usize) -> Option<SassResult<Spanned<Value>>> {
|
||||
self.get_positional(position)
|
||||
}
|
||||
|
||||
pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> SassResult<Value> {
|
||||
Ok(match self.get_named(name) {
|
||||
Some(val) => val?.node,
|
||||
None => default,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
|
||||
let mut vals = Vec::new();
|
||||
let mut args = match self
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(a, v)| Ok((a.position()?, v)))
|
||||
.collect::<Result<Vec<(usize, SassResult<Spanned<Value>>)>, String>>()
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err((format!("No argument named ${}.", e), self.1).into()),
|
||||
};
|
||||
|
||||
args.sort_by(|(a1, _), (a2, _)| a1.cmp(a2));
|
||||
|
||||
for (_, arg) in args {
|
||||
vals.push(arg?);
|
||||
}
|
||||
|
||||
Ok(vals)
|
||||
}
|
||||
}
|
298
src/ast/args.rs
Normal file
298
src/ast/args.rs
Normal file
@ -0,0 +1,298 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
iter::Iterator,
|
||||
mem,
|
||||
};
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
common::{Identifier, ListSeparator},
|
||||
error::SassResult,
|
||||
utils::to_sentence,
|
||||
value::Value,
|
||||
};
|
||||
|
||||
use super::AstExpr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Argument {
|
||||
pub name: Identifier,
|
||||
pub default: Option<AstExpr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ArgumentDeclaration {
|
||||
pub args: Vec<Argument>,
|
||||
pub rest: Option<Identifier>,
|
||||
}
|
||||
|
||||
impl ArgumentDeclaration {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
args: Vec::new(),
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify<T>(
|
||||
&self,
|
||||
num_positional: usize,
|
||||
names: &BTreeMap<Identifier, T>,
|
||||
span: Span,
|
||||
) -> SassResult<()> {
|
||||
let mut named_used = 0;
|
||||
|
||||
for i in 0..self.args.len() {
|
||||
let argument = &self.args[i];
|
||||
|
||||
if i < num_positional {
|
||||
if names.contains_key(&argument.name) {
|
||||
// todo: _originalArgumentName
|
||||
return Err((
|
||||
format!(
|
||||
"Argument ${} was passed both by position and by name.",
|
||||
argument.name
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else if names.contains_key(&argument.name) {
|
||||
named_used += 1;
|
||||
} else if argument.default.is_none() {
|
||||
// todo: _originalArgumentName
|
||||
return Err((format!("Missing argument ${}.", argument.name), span).into());
|
||||
}
|
||||
}
|
||||
|
||||
if self.rest.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if num_positional > self.args.len() {
|
||||
return Err((
|
||||
format!(
|
||||
"Only {} {}{} allowed, but {num_positional} {} passed.",
|
||||
self.args.len(),
|
||||
if names.is_empty() { "" } else { "positional " },
|
||||
if self.args.len() == 1 {
|
||||
"argument"
|
||||
} else {
|
||||
"arguments"
|
||||
},
|
||||
if num_positional == 1 { "was" } else { "were" }
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if named_used < names.len() {
|
||||
let mut unknown_names = names.keys().copied().collect::<BTreeSet<_>>();
|
||||
|
||||
for arg in &self.args {
|
||||
unknown_names.remove(&arg.name);
|
||||
}
|
||||
|
||||
if unknown_names.len() == 1 {
|
||||
return Err((
|
||||
format!(
|
||||
"No argument named ${}.",
|
||||
unknown_names.iter().next().unwrap()
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if unknown_names.len() > 1 {
|
||||
return Err((
|
||||
format!(
|
||||
"No arguments named {}.",
|
||||
to_sentence(
|
||||
unknown_names
|
||||
.into_iter()
|
||||
.map(|name| format!("${name}"))
|
||||
.collect(),
|
||||
"or"
|
||||
)
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ArgumentInvocation {
|
||||
pub positional: Vec<AstExpr>,
|
||||
pub named: BTreeMap<Identifier, AstExpr>,
|
||||
pub rest: Option<AstExpr>,
|
||||
pub keyword_rest: Option<AstExpr>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl ArgumentInvocation {
|
||||
pub fn empty(span: Span) -> Self {
|
||||
Self {
|
||||
positional: Vec::new(),
|
||||
named: BTreeMap::new(),
|
||||
rest: None,
|
||||
keyword_rest: None,
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: hack for builtin `call`
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum MaybeEvaledArguments {
|
||||
Invocation(ArgumentInvocation),
|
||||
Evaled(ArgumentResult),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ArgumentResult {
|
||||
pub positional: Vec<Value>,
|
||||
pub named: BTreeMap<Identifier, Value>,
|
||||
pub separator: ListSeparator,
|
||||
pub span: Span,
|
||||
// todo: hack
|
||||
pub touched: BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl ArgumentResult {
|
||||
/// Get argument by name
|
||||
///
|
||||
/// Removes the argument
|
||||
pub fn get_named<T: Into<Identifier>>(&mut self, val: T) -> Option<Spanned<Value>> {
|
||||
self.named.remove(&val.into()).map(|n| Spanned {
|
||||
node: n,
|
||||
span: self.span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a positional argument by 0-indexed position
|
||||
///
|
||||
/// Replaces argument with `Value::Null` gravestone
|
||||
pub fn get_positional(&mut self, idx: usize) -> Option<Spanned<Value>> {
|
||||
let val = match self.positional.get_mut(idx) {
|
||||
Some(v) => Some(Spanned {
|
||||
node: mem::replace(v, Value::Null),
|
||||
span: self.span,
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.touched.insert(idx);
|
||||
val
|
||||
}
|
||||
|
||||
pub fn get<T: Into<Identifier>>(&mut self, position: usize, name: T) -> Option<Spanned<Value>> {
|
||||
match self.get_named(name) {
|
||||
Some(v) => Some(v),
|
||||
None => self.get_positional(position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_err(&mut self, position: usize, name: &'static str) -> SassResult<Value> {
|
||||
match self.get_named(name) {
|
||||
Some(v) => Ok(v.node),
|
||||
None => match self.get_positional(position) {
|
||||
Some(v) => Ok(v.node),
|
||||
None => Err((format!("Missing argument ${}.", name), self.span()).into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn span(&self) -> Span {
|
||||
self.span
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.positional.len() + self.named.len()
|
||||
}
|
||||
|
||||
pub fn min_args(&self, min: usize) -> SassResult<()> {
|
||||
let len = self.len();
|
||||
if len < min {
|
||||
if min == 1 {
|
||||
return Err(("At least one argument must be passed.", self.span()).into());
|
||||
}
|
||||
todo!("min args greater than one")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn max_args(&self, max: usize) -> SassResult<()> {
|
||||
let len = self.len();
|
||||
if len > max {
|
||||
let mut err = String::with_capacity(50);
|
||||
#[allow(clippy::format_push_string)]
|
||||
err.push_str(&format!("Only {} argument", max));
|
||||
if max != 1 {
|
||||
err.push('s');
|
||||
}
|
||||
err.push_str(" allowed, but ");
|
||||
err.push_str(&len.to_string());
|
||||
err.push(' ');
|
||||
if len == 1 {
|
||||
err.push_str("was passed.");
|
||||
} else {
|
||||
err.push_str("were passed.");
|
||||
}
|
||||
return Err((err, self.span()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_arg(&mut self, position: usize, name: &'static str, default: Value) -> Value {
|
||||
match self.get(position, name) {
|
||||
Some(val) => val.node,
|
||||
None => default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_positional(&mut self, position: usize) -> Option<Value> {
|
||||
if self.positional.len() > position {
|
||||
Some(self.positional.remove(position))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> Value {
|
||||
match self.get_named(name) {
|
||||
Some(val) => val.node,
|
||||
None => default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
|
||||
if let Some((name, _)) = self.named.iter().next() {
|
||||
return Err((format!("No argument named ${}.", name), self.span).into());
|
||||
}
|
||||
|
||||
let Self {
|
||||
positional,
|
||||
span,
|
||||
touched,
|
||||
..
|
||||
} = self;
|
||||
|
||||
// todo: complete hack, we shouldn't have the `touched` set
|
||||
let args = positional
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| !touched.contains(idx))
|
||||
.map(|(_, node)| Spanned { node, span })
|
||||
.collect();
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
}
|
129
src/ast/css.rs
Normal file
129
src/ast/css.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use codemap::Span;
|
||||
|
||||
use crate::selector::ExtendedSelector;
|
||||
|
||||
use super::{MediaRule, Style, UnknownAtRule};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum CssStmt {
|
||||
RuleSet {
|
||||
selector: ExtendedSelector,
|
||||
body: Vec<Self>,
|
||||
is_group_end: bool,
|
||||
},
|
||||
Style(Style),
|
||||
Media(MediaRule, bool),
|
||||
UnknownAtRule(UnknownAtRule, bool),
|
||||
Supports(SupportsRule, bool),
|
||||
Comment(String, Span),
|
||||
KeyframesRuleSet(KeyframesRuleSet),
|
||||
/// A plain import such as `@import "foo.css";` or
|
||||
/// `@import url(https://fonts.google.com/foo?bar);`
|
||||
// todo: named fields, 0: url, 1: modifiers
|
||||
Import(String, Option<String>),
|
||||
}
|
||||
|
||||
impl CssStmt {
|
||||
pub fn is_style_rule(&self) -> bool {
|
||||
matches!(self, CssStmt::RuleSet { .. })
|
||||
}
|
||||
|
||||
pub fn set_group_end(&mut self) {
|
||||
match self {
|
||||
CssStmt::Media(_, is_group_end)
|
||||
| CssStmt::UnknownAtRule(_, is_group_end)
|
||||
| CssStmt::Supports(_, is_group_end)
|
||||
| CssStmt::RuleSet { is_group_end, .. } => *is_group_end = true,
|
||||
CssStmt::Style(_)
|
||||
| CssStmt::Comment(_, _)
|
||||
| CssStmt::KeyframesRuleSet(_)
|
||||
| CssStmt::Import(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_group_end(&self) -> bool {
|
||||
match self {
|
||||
CssStmt::Media(_, is_group_end)
|
||||
| CssStmt::UnknownAtRule(_, is_group_end)
|
||||
| CssStmt::Supports(_, is_group_end)
|
||||
| CssStmt::RuleSet { is_group_end, .. } => *is_group_end,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_invisible(&self) -> bool {
|
||||
match self {
|
||||
CssStmt::RuleSet { selector, body, .. } => {
|
||||
selector.is_invisible() || body.iter().all(CssStmt::is_invisible)
|
||||
}
|
||||
CssStmt::Style(style) => style.value.node.is_null(),
|
||||
CssStmt::Media(media_rule, ..) => media_rule.body.iter().all(CssStmt::is_invisible),
|
||||
CssStmt::UnknownAtRule(..) | CssStmt::Import(..) | CssStmt::Comment(..) => false,
|
||||
CssStmt::Supports(supports_rule, ..) => {
|
||||
supports_rule.body.iter().all(CssStmt::is_invisible)
|
||||
}
|
||||
CssStmt::KeyframesRuleSet(kf) => kf.body.iter().all(CssStmt::is_invisible),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_without_children(&self) -> Self {
|
||||
match self {
|
||||
CssStmt::RuleSet {
|
||||
selector,
|
||||
is_group_end,
|
||||
..
|
||||
} => CssStmt::RuleSet {
|
||||
selector: selector.clone(),
|
||||
body: Vec::new(),
|
||||
is_group_end: *is_group_end,
|
||||
},
|
||||
CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..) => unreachable!(),
|
||||
CssStmt::Media(media, is_group_end) => CssStmt::Media(
|
||||
MediaRule {
|
||||
query: media.query.clone(),
|
||||
body: Vec::new(),
|
||||
},
|
||||
*is_group_end,
|
||||
),
|
||||
CssStmt::UnknownAtRule(at_rule, is_group_end) => CssStmt::UnknownAtRule(
|
||||
UnknownAtRule {
|
||||
name: at_rule.name.clone(),
|
||||
params: at_rule.params.clone(),
|
||||
body: Vec::new(),
|
||||
has_body: at_rule.has_body,
|
||||
},
|
||||
*is_group_end,
|
||||
),
|
||||
CssStmt::Supports(supports, is_group_end) => CssStmt::Supports(
|
||||
SupportsRule {
|
||||
params: supports.params.clone(),
|
||||
body: Vec::new(),
|
||||
},
|
||||
*is_group_end,
|
||||
),
|
||||
CssStmt::KeyframesRuleSet(keyframes) => CssStmt::KeyframesRuleSet(KeyframesRuleSet {
|
||||
selector: keyframes.selector.clone(),
|
||||
body: Vec::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KeyframesRuleSet {
|
||||
pub selector: Vec<KeyframesSelector>,
|
||||
pub body: Vec<CssStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum KeyframesSelector {
|
||||
To,
|
||||
From,
|
||||
Percent(Box<str>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SupportsRule {
|
||||
pub params: String,
|
||||
pub body: Vec<CssStmt>,
|
||||
}
|
188
src/ast/expr.rs
Normal file
188
src/ast/expr.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use std::iter::Iterator;
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp},
|
||||
unit::Unit,
|
||||
value::{CalculationName, Number},
|
||||
};
|
||||
|
||||
use super::{ArgumentInvocation, AstSupportsCondition, Interpolation, InterpolationPart};
|
||||
|
||||
/// Represented by the `if` function
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Ternary(pub ArgumentInvocation);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ListExpr {
|
||||
pub elems: Vec<Spanned<AstExpr>>,
|
||||
pub separator: ListSeparator,
|
||||
pub brackets: Brackets,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct FunctionCallExpr {
|
||||
pub namespace: Option<Spanned<Identifier>>,
|
||||
pub name: Identifier,
|
||||
pub arguments: Box<ArgumentInvocation>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct InterpolatedFunction {
|
||||
pub name: Interpolation,
|
||||
pub arguments: Box<ArgumentInvocation>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct AstSassMap(pub Vec<(Spanned<AstExpr>, AstExpr)>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum AstExpr {
|
||||
BinaryOp {
|
||||
lhs: Box<Self>,
|
||||
op: BinaryOp,
|
||||
rhs: Box<Self>,
|
||||
allows_slash: bool,
|
||||
span: Span,
|
||||
},
|
||||
True,
|
||||
False,
|
||||
Calculation {
|
||||
name: CalculationName,
|
||||
args: Vec<Self>,
|
||||
},
|
||||
Color(Box<Color>),
|
||||
FunctionCall(FunctionCallExpr),
|
||||
If(Box<Ternary>),
|
||||
InterpolatedFunction(InterpolatedFunction),
|
||||
List(ListExpr),
|
||||
Map(AstSassMap),
|
||||
Null,
|
||||
Number {
|
||||
n: Number,
|
||||
unit: Unit,
|
||||
},
|
||||
Paren(Box<Self>),
|
||||
ParentSelector,
|
||||
String(StringExpr, Span),
|
||||
Supports(Box<AstSupportsCondition>),
|
||||
UnaryOp(UnaryOp, Box<Self>, Span),
|
||||
Variable {
|
||||
name: Spanned<Identifier>,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
},
|
||||
}
|
||||
|
||||
// todo: make quotes bool
|
||||
// todo: track span inside
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StringExpr(pub Interpolation, pub QuoteKind);
|
||||
|
||||
impl StringExpr {
|
||||
fn quote_inner_text(
|
||||
text: &str,
|
||||
quote: char,
|
||||
buffer: &mut Interpolation,
|
||||
// default=false
|
||||
is_static: bool,
|
||||
) {
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(char) = chars.next() {
|
||||
if char == '\n' || char == '\r' {
|
||||
buffer.add_char('\\');
|
||||
buffer.add_char('a');
|
||||
if let Some(next) = chars.peek() {
|
||||
if next.is_ascii_whitespace() || next.is_ascii_hexdigit() {
|
||||
buffer.add_char(' ');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if char == quote
|
||||
|| char == '\\'
|
||||
|| (is_static && char == '#' && chars.peek() == Some(&'{'))
|
||||
{
|
||||
buffer.add_char('\\');
|
||||
}
|
||||
buffer.add_char(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn best_quote<'a>(strings: impl Iterator<Item = &'a str>) -> char {
|
||||
let mut contains_double_quote = false;
|
||||
for s in strings {
|
||||
for c in s.chars() {
|
||||
if c == '\'' {
|
||||
return '"';
|
||||
}
|
||||
if c == '"' {
|
||||
contains_double_quote = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if contains_double_quote {
|
||||
'\''
|
||||
} else {
|
||||
'"'
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_interpolation(self, is_static: bool) -> Interpolation {
|
||||
if self.1 == QuoteKind::None {
|
||||
return self.0;
|
||||
}
|
||||
|
||||
let quote = Self::best_quote(self.0.contents.iter().filter_map(|c| match c {
|
||||
InterpolationPart::Expr(..) => None,
|
||||
InterpolationPart::String(text) => Some(text.as_str()),
|
||||
}));
|
||||
|
||||
let mut buffer = Interpolation::new();
|
||||
buffer.add_char(quote);
|
||||
|
||||
for value in self.0.contents {
|
||||
match value {
|
||||
InterpolationPart::Expr(e) => buffer.add_expr(e),
|
||||
InterpolationPart::String(text) => {
|
||||
Self::quote_inner_text(&text, quote, &mut buffer, is_static);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.add_char(quote);
|
||||
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl AstExpr {
|
||||
pub fn is_variable(&self) -> bool {
|
||||
matches!(self, Self::Variable { .. })
|
||||
}
|
||||
|
||||
pub fn is_slash_operand(&self) -> bool {
|
||||
match self {
|
||||
Self::Number { .. } | Self::Calculation { .. } => true,
|
||||
Self::BinaryOp { allows_slash, .. } => *allows_slash,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slash(left: Self, right: Self, span: Span) -> Self {
|
||||
Self::BinaryOp {
|
||||
lhs: Box::new(left),
|
||||
op: BinaryOp::Div,
|
||||
rhs: Box::new(right),
|
||||
allows_slash: true,
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn span(self, span: Span) -> Spanned<Self> {
|
||||
Spanned { node: self, span }
|
||||
}
|
||||
}
|
99
src/ast/interpolation.rs
Normal file
99
src/ast/interpolation.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::token::Token;
|
||||
|
||||
use super::AstExpr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Interpolation {
|
||||
pub contents: Vec<InterpolationPart>,
|
||||
}
|
||||
|
||||
impl Interpolation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
contents: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.contents.is_empty()
|
||||
}
|
||||
|
||||
pub fn new_with_expr(e: Spanned<AstExpr>) -> Self {
|
||||
Self {
|
||||
contents: vec![InterpolationPart::Expr(e)],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_plain(s: String) -> Self {
|
||||
Self {
|
||||
contents: vec![InterpolationPart::String(s)],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_expr(&mut self, expr: Spanned<AstExpr>) {
|
||||
self.contents.push(InterpolationPart::Expr(expr));
|
||||
}
|
||||
|
||||
// todo: cow?
|
||||
pub fn add_string(&mut self, s: String) {
|
||||
match self.contents.last_mut() {
|
||||
Some(InterpolationPart::String(existing)) => *existing += &s,
|
||||
_ => self.contents.push(InterpolationPart::String(s)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_token(&mut self, tok: Token) {
|
||||
match self.contents.last_mut() {
|
||||
Some(InterpolationPart::String(existing)) => existing.push(tok.kind),
|
||||
_ => self
|
||||
.contents
|
||||
.push(InterpolationPart::String(tok.kind.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_char(&mut self, c: char) {
|
||||
match self.contents.last_mut() {
|
||||
Some(InterpolationPart::String(existing)) => existing.push(c),
|
||||
_ => self.contents.push(InterpolationPart::String(c.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_interpolation(&mut self, mut other: Self) {
|
||||
self.contents.append(&mut other.contents);
|
||||
}
|
||||
|
||||
pub fn initial_plain(&self) -> &str {
|
||||
match self.contents.first() {
|
||||
Some(InterpolationPart::String(s)) => s,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_plain(&self) -> Option<&str> {
|
||||
if self.contents.is_empty() {
|
||||
Some("")
|
||||
} else if self.contents.len() > 1 {
|
||||
None
|
||||
} else {
|
||||
match self.contents.first()? {
|
||||
InterpolationPart::String(s) => Some(s),
|
||||
InterpolationPart::Expr(..) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trailing_string(&self) -> &str {
|
||||
match self.contents.last() {
|
||||
Some(InterpolationPart::String(s)) => s,
|
||||
Some(InterpolationPart::Expr(..)) | None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum InterpolationPart {
|
||||
String(String),
|
||||
Expr(Spanned<AstExpr>),
|
||||
}
|
@ -1,36 +1,24 @@
|
||||
#![allow(dead_code)]
|
||||
use std::fmt;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
use crate::{parse::Stmt, selector::Selector};
|
||||
use codemap::Span;
|
||||
|
||||
use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser, token::Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MediaRule {
|
||||
pub super_selector: Selector,
|
||||
pub query: String,
|
||||
pub body: Vec<Stmt>,
|
||||
pub query: Vec<MediaQuery>,
|
||||
pub body: Vec<CssStmt>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct MediaQuery {
|
||||
/// The modifier, probably either "not" or "only".
|
||||
///
|
||||
/// This may be `None` if no modifier is in use.
|
||||
pub modifier: Option<String>,
|
||||
|
||||
/// The media type, for example "screen" or "print".
|
||||
///
|
||||
/// This may be `None`. If so, `self.features` will not be empty.
|
||||
pub media_type: Option<String>,
|
||||
|
||||
/// Feature queries, including parentheses.
|
||||
pub features: Vec<String>,
|
||||
pub conditions: Vec<String>,
|
||||
pub conjunction: bool,
|
||||
}
|
||||
|
||||
impl MediaQuery {
|
||||
pub fn is_condition(&self) -> bool {
|
||||
self.modifier.is_none() && self.media_type.is_none()
|
||||
}
|
||||
|
||||
pub fn matches_all_types(&self) -> bool {
|
||||
self.media_type.is_none()
|
||||
|| self
|
||||
@ -39,16 +27,44 @@ impl MediaQuery {
|
||||
.map_or(false, |v| v.to_ascii_lowercase() == "all")
|
||||
}
|
||||
|
||||
pub fn condition(features: Vec<String>) -> Self {
|
||||
pub fn condition(
|
||||
conditions: Vec<String>,
|
||||
// default=true
|
||||
conjunction: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
modifier: None,
|
||||
media_type: None,
|
||||
features,
|
||||
conditions,
|
||||
conjunction,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn media_type(
|
||||
media_type: Option<String>,
|
||||
modifier: Option<String>,
|
||||
conditions: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
modifier,
|
||||
conjunction: true,
|
||||
media_type,
|
||||
conditions: conditions.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_list(list: &str, span: Span) -> SassResult<Vec<Self>> {
|
||||
let toks = Lexer::new(list.chars().map(|x| Token::new(span, x)).collect());
|
||||
|
||||
MediaQueryParser::new(toks).parse()
|
||||
}
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
fn merge(&self, other: &Self) -> MediaQueryMergeResult {
|
||||
pub fn merge(&self, other: &Self) -> MediaQueryMergeResult {
|
||||
if !self.conjunction || !other.conjunction {
|
||||
return MediaQueryMergeResult::Unrepresentable;
|
||||
}
|
||||
|
||||
let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
||||
let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase());
|
||||
let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
||||
@ -56,33 +72,34 @@ impl MediaQuery {
|
||||
|
||||
if this_type.is_none() && other_type.is_none() {
|
||||
return MediaQueryMergeResult::Success(Self::condition(
|
||||
self.features
|
||||
self.conditions
|
||||
.iter()
|
||||
.chain(&other.features)
|
||||
.chain(&other.conditions)
|
||||
.cloned()
|
||||
.collect(),
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
let modifier;
|
||||
let media_type;
|
||||
let features;
|
||||
let conditions;
|
||||
|
||||
if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) {
|
||||
if this_modifier == other_modifier {
|
||||
let negative_features = if this_modifier.as_deref() == Some("not") {
|
||||
&self.features
|
||||
let negative_conditions = if this_modifier.as_deref() == Some("not") {
|
||||
&self.conditions
|
||||
} else {
|
||||
&other.features
|
||||
&other.conditions
|
||||
};
|
||||
|
||||
let positive_features = if this_modifier.as_deref() == Some("not") {
|
||||
&other.features
|
||||
let positive_conditions = if this_modifier.as_deref() == Some("not") {
|
||||
&other.conditions
|
||||
} else {
|
||||
&self.features
|
||||
&self.conditions
|
||||
};
|
||||
|
||||
// If the negative features are a subset of the positive features, the
|
||||
// If the negative conditions are a subset of the positive conditions, the
|
||||
// query is empty. For example, `not screen and (color)` has no
|
||||
// intersection with `screen and (color) and (grid)`.
|
||||
//
|
||||
@ -90,9 +107,9 @@ impl MediaQuery {
|
||||
// (grid)`, because it means `not (screen and (color))` and so it allows
|
||||
// a screen with no color but with a grid.
|
||||
|
||||
if negative_features
|
||||
if negative_conditions
|
||||
.iter()
|
||||
.all(|feat| positive_features.contains(feat))
|
||||
.all(|feat| positive_conditions.contains(feat))
|
||||
{
|
||||
return MediaQueryMergeResult::Empty;
|
||||
}
|
||||
@ -105,11 +122,11 @@ impl MediaQuery {
|
||||
if this_modifier.as_deref() == Some("not") {
|
||||
modifier = &other_modifier;
|
||||
media_type = &other_type;
|
||||
features = other.features.clone();
|
||||
conditions = other.conditions.clone();
|
||||
} else {
|
||||
modifier = &this_modifier;
|
||||
media_type = &this_type;
|
||||
features = self.features.clone();
|
||||
conditions = self.conditions.clone();
|
||||
}
|
||||
} else if this_modifier.as_deref() == Some("not") {
|
||||
debug_assert_eq!(other_modifier.as_deref(), Some("not"));
|
||||
@ -119,27 +136,27 @@ impl MediaQuery {
|
||||
return MediaQueryMergeResult::Unrepresentable;
|
||||
}
|
||||
|
||||
let more_features = if self.features.len() > other.features.len() {
|
||||
&self.features
|
||||
let more_conditions = if self.conditions.len() > other.conditions.len() {
|
||||
&self.conditions
|
||||
} else {
|
||||
&other.features
|
||||
&other.conditions
|
||||
};
|
||||
|
||||
let fewer_features = if self.features.len() > other.features.len() {
|
||||
&other.features
|
||||
let fewer_conditions = if self.conditions.len() > other.conditions.len() {
|
||||
&other.conditions
|
||||
} else {
|
||||
&self.features
|
||||
&self.conditions
|
||||
};
|
||||
|
||||
// If one set of features is a superset of the other, use those features
|
||||
// If one set of conditions is a superset of the other, use those conditions
|
||||
// because they're strictly narrower.
|
||||
if fewer_features
|
||||
if fewer_conditions
|
||||
.iter()
|
||||
.all(|feat| more_features.contains(feat))
|
||||
.all(|feat| more_conditions.contains(feat))
|
||||
{
|
||||
modifier = &this_modifier; // "not"
|
||||
modifier = &this_modifier;
|
||||
media_type = &this_type;
|
||||
features = more_features.clone();
|
||||
conditions = more_conditions.clone();
|
||||
} else {
|
||||
// Otherwise, there's no way to represent the intersection.
|
||||
return MediaQueryMergeResult::Unrepresentable;
|
||||
@ -155,19 +172,19 @@ impl MediaQuery {
|
||||
&other_type
|
||||
};
|
||||
|
||||
features = self
|
||||
.features
|
||||
conditions = self
|
||||
.conditions
|
||||
.iter()
|
||||
.chain(&other.features)
|
||||
.chain(&other.conditions)
|
||||
.cloned()
|
||||
.collect();
|
||||
} else if other.matches_all_types() {
|
||||
modifier = &this_modifier;
|
||||
media_type = &this_type;
|
||||
features = self
|
||||
.features
|
||||
conditions = self
|
||||
.conditions
|
||||
.iter()
|
||||
.chain(&other.features)
|
||||
.chain(&other.conditions)
|
||||
.cloned()
|
||||
.collect();
|
||||
} else if this_type != other_type {
|
||||
@ -180,10 +197,10 @@ impl MediaQuery {
|
||||
}
|
||||
|
||||
media_type = &this_type;
|
||||
features = self
|
||||
.features
|
||||
conditions = self
|
||||
.conditions
|
||||
.iter()
|
||||
.chain(&other.features)
|
||||
.chain(&other.conditions)
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
@ -199,7 +216,8 @@ impl MediaQuery {
|
||||
} else {
|
||||
other.modifier.clone()
|
||||
},
|
||||
features,
|
||||
conditions,
|
||||
conjunction: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -208,19 +226,22 @@ impl fmt::Display for MediaQuery {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(modifier) = &self.modifier {
|
||||
f.write_str(modifier)?;
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
|
||||
if let Some(media_type) = &self.media_type {
|
||||
f.write_str(media_type)?;
|
||||
if !&self.features.is_empty() {
|
||||
if !&self.conditions.is_empty() {
|
||||
f.write_str(" and ")?;
|
||||
}
|
||||
}
|
||||
f.write_str(&self.features.join(" and "))
|
||||
|
||||
f.write_str(&self.conditions.join(" and "))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum MediaQueryMergeResult {
|
||||
pub(crate) enum MediaQueryMergeResult {
|
||||
Empty,
|
||||
Unrepresentable,
|
||||
Success(MediaQuery),
|
32
src/ast/mixin.rs
Normal file
32
src/ast/mixin.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
ast::ArgumentResult,
|
||||
error::SassResult,
|
||||
evaluate::{Environment, Visitor},
|
||||
};
|
||||
|
||||
pub(crate) type BuiltinMixin = fn(ArgumentResult, &mut Visitor) -> SassResult<()>;
|
||||
|
||||
pub(crate) use crate::ast::AstMixin as UserDefinedMixin;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Mixin {
|
||||
UserDefined(UserDefinedMixin, Environment),
|
||||
Builtin(BuiltinMixin),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Mixin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UserDefined(u, ..) => f
|
||||
.debug_struct("AstMixin")
|
||||
.field("name", &u.name)
|
||||
.field("args", &u.args)
|
||||
.field("body", &u.body)
|
||||
.field("has_content", &u.has_content)
|
||||
.finish(),
|
||||
Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(),
|
||||
}
|
||||
}
|
||||
}
|
19
src/ast/mod.rs
Normal file
19
src/ast/mod.rs
Normal file
@ -0,0 +1,19 @@
|
||||
pub(crate) use args::*;
|
||||
pub(crate) use css::*;
|
||||
pub(crate) use expr::*;
|
||||
pub(crate) use interpolation::*;
|
||||
pub(crate) use media::*;
|
||||
pub(crate) use mixin::*;
|
||||
pub(crate) use stmt::*;
|
||||
pub(crate) use style::*;
|
||||
pub(crate) use unknown::*;
|
||||
|
||||
mod args;
|
||||
mod css;
|
||||
mod expr;
|
||||
mod interpolation;
|
||||
mod media;
|
||||
mod mixin;
|
||||
mod stmt;
|
||||
mod style;
|
||||
mod unknown;
|
568
src/ast/stmt.rs
Normal file
568
src/ast/stmt.rs
Normal file
@ -0,0 +1,568 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, HashSet},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
ast::{ArgumentDeclaration, ArgumentInvocation, AstExpr, CssStmt},
|
||||
ast::{Interpolation, MediaQuery},
|
||||
common::Identifier,
|
||||
utils::{BaseMapView, LimitedMapView, MapView, UnprefixedMapView},
|
||||
value::Value,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(unused)]
|
||||
pub(crate) struct AstSilentComment {
|
||||
pub text: String,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstPlainCssImport {
|
||||
pub url: Interpolation,
|
||||
pub modifiers: Option<Interpolation>,
|
||||
#[allow(unused)]
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstSassImport {
|
||||
pub url: String,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstIf {
|
||||
pub if_clauses: Vec<AstIfClause>,
|
||||
pub else_clause: Option<Vec<AstStmt>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstIfClause {
|
||||
pub condition: AstExpr,
|
||||
pub body: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstFor {
|
||||
pub variable: Spanned<Identifier>,
|
||||
pub from: Spanned<AstExpr>,
|
||||
pub to: Spanned<AstExpr>,
|
||||
pub is_exclusive: bool,
|
||||
pub body: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstReturn {
|
||||
pub val: AstExpr,
|
||||
#[allow(unused)]
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstRuleSet {
|
||||
pub selector: Interpolation,
|
||||
pub body: Vec<AstStmt>,
|
||||
pub selector_span: Span,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstStyle {
|
||||
pub name: Interpolation,
|
||||
pub value: Option<Spanned<AstExpr>>,
|
||||
pub body: Vec<AstStmt>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl AstStyle {
|
||||
pub fn is_custom_property(&self) -> bool {
|
||||
self.name.initial_plain().starts_with("--")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstEach {
|
||||
pub variables: Vec<Identifier>,
|
||||
pub list: AstExpr,
|
||||
pub body: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstMedia {
|
||||
pub query: Interpolation,
|
||||
pub body: Vec<AstStmt>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
pub(crate) type CssMediaQuery = MediaQuery;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstWhile {
|
||||
pub condition: AstExpr,
|
||||
pub body: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstVariableDecl {
|
||||
pub namespace: Option<Spanned<Identifier>>,
|
||||
pub name: Identifier,
|
||||
pub value: AstExpr,
|
||||
pub is_guarded: bool,
|
||||
pub is_global: bool,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstFunctionDecl {
|
||||
pub name: Spanned<Identifier>,
|
||||
pub arguments: ArgumentDeclaration,
|
||||
pub children: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstDebugRule {
|
||||
pub value: AstExpr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstWarn {
|
||||
pub value: AstExpr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstErrorRule {
|
||||
pub value: AstExpr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl PartialEq for AstFunctionDecl {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AstFunctionDecl {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstLoudComment {
|
||||
pub text: Interpolation,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstMixin {
|
||||
pub name: Identifier,
|
||||
pub args: ArgumentDeclaration,
|
||||
pub body: Vec<AstStmt>,
|
||||
/// Whether the mixin contains a `@content` rule.
|
||||
pub has_content: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstContentRule {
|
||||
pub args: ArgumentInvocation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstContentBlock {
|
||||
pub args: ArgumentDeclaration,
|
||||
pub body: Vec<AstStmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstInclude {
|
||||
pub namespace: Option<Spanned<Identifier>>,
|
||||
pub name: Spanned<Identifier>,
|
||||
pub args: ArgumentInvocation,
|
||||
pub content: Option<AstContentBlock>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstUnknownAtRule {
|
||||
pub name: Interpolation,
|
||||
pub value: Option<Interpolation>,
|
||||
pub children: Option<Vec<AstStmt>>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstExtendRule {
|
||||
pub value: Interpolation,
|
||||
pub is_optional: bool,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstAtRootRule {
|
||||
pub children: Vec<AstStmt>,
|
||||
pub query: Option<Interpolation>,
|
||||
#[allow(unused)]
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AtRootQuery {
|
||||
pub include: bool,
|
||||
pub names: HashSet<String>,
|
||||
pub all: bool,
|
||||
pub rule: bool,
|
||||
}
|
||||
|
||||
impl AtRootQuery {
|
||||
pub fn new(include: bool, names: HashSet<String>) -> Self {
|
||||
let all = names.contains("all");
|
||||
let rule = names.contains("rule");
|
||||
|
||||
Self {
|
||||
include,
|
||||
names,
|
||||
all,
|
||||
rule,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn excludes_name(&self, name: &str) -> bool {
|
||||
(self.all || self.names.contains(name)) != self.include
|
||||
}
|
||||
|
||||
pub fn excludes_style_rules(&self) -> bool {
|
||||
(self.all || self.rule) != self.include
|
||||
}
|
||||
|
||||
pub fn excludes(&self, stmt: &CssStmt) -> bool {
|
||||
if self.all {
|
||||
return !self.include;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
CssStmt::RuleSet { .. } => self.excludes_style_rules(),
|
||||
CssStmt::Media(..) => self.excludes_name("media"),
|
||||
CssStmt::Supports(..) => self.excludes_name("supports"),
|
||||
CssStmt::UnknownAtRule(rule, ..) => self.excludes_name(&rule.name.to_ascii_lowercase()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AtRootQuery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
include: false,
|
||||
names: HashSet::new(),
|
||||
all: false,
|
||||
rule: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstImportRule {
|
||||
pub imports: Vec<AstImport>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum AstImport {
|
||||
Plain(AstPlainCssImport),
|
||||
Sass(AstSassImport),
|
||||
}
|
||||
|
||||
impl AstImport {
|
||||
pub fn is_dynamic(&self) -> bool {
|
||||
matches!(self, AstImport::Sass(..))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstUseRule {
|
||||
pub url: PathBuf,
|
||||
pub namespace: Option<String>,
|
||||
pub configuration: Vec<ConfiguredVariable>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConfiguredVariable {
|
||||
pub name: Spanned<Identifier>,
|
||||
pub expr: Spanned<AstExpr>,
|
||||
pub is_guarded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Configuration {
|
||||
pub values: Arc<dyn MapView<Value = ConfiguredValue>>,
|
||||
#[allow(unused)]
|
||||
pub original_config: Option<Arc<RefCell<Self>>>,
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn through_forward(
|
||||
config: Arc<RefCell<Self>>,
|
||||
forward: &AstForwardRule,
|
||||
) -> Arc<RefCell<Self>> {
|
||||
if (*config).borrow().is_empty() {
|
||||
return Arc::new(RefCell::new(Configuration::empty()));
|
||||
}
|
||||
|
||||
let mut new_values = Arc::clone(&(*config).borrow().values);
|
||||
|
||||
// Only allow variables that are visible through the `@forward` to be
|
||||
// configured. These views support [Map.remove] so we can mark when a
|
||||
// configuration variable is used by removing it even when the underlying
|
||||
// map is wrapped.
|
||||
if let Some(prefix) = &forward.prefix {
|
||||
new_values = Arc::new(UnprefixedMapView(new_values, prefix.clone()));
|
||||
}
|
||||
|
||||
if let Some(shown_variables) = &forward.shown_variables {
|
||||
new_values = Arc::new(LimitedMapView::safelist(new_values, shown_variables));
|
||||
} else if let Some(hidden_variables) = &forward.hidden_variables {
|
||||
new_values = Arc::new(LimitedMapView::blocklist(new_values, hidden_variables));
|
||||
}
|
||||
|
||||
Arc::new(RefCell::new(Self::with_values(
|
||||
config,
|
||||
Arc::clone(&new_values),
|
||||
)))
|
||||
}
|
||||
|
||||
fn with_values(
|
||||
config: Arc<RefCell<Self>>,
|
||||
values: Arc<dyn MapView<Value = ConfiguredValue>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
values,
|
||||
original_config: Some(config),
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn first(&self) -> Option<Spanned<Identifier>> {
|
||||
let name = *self.values.keys().get(0)?;
|
||||
let value = self.values.get(name)?;
|
||||
|
||||
Some(Spanned {
|
||||
node: name,
|
||||
span: value.configuration_span?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, name: Identifier) -> Option<ConfiguredValue> {
|
||||
self.values.remove(name)
|
||||
}
|
||||
|
||||
pub fn is_implicit(&self) -> bool {
|
||||
self.span.is_none()
|
||||
}
|
||||
|
||||
pub fn implicit(values: BTreeMap<Identifier, ConfiguredValue>) -> Self {
|
||||
Self {
|
||||
values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))),
|
||||
original_config: None,
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explicit(values: BTreeMap<Identifier, ConfiguredValue>, span: Span) -> Self {
|
||||
Self {
|
||||
values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))),
|
||||
original_config: None,
|
||||
span: Some(span),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
values: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||
original_config: None,
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn original_config(config: Arc<RefCell<Configuration>>) -> Arc<RefCell<Configuration>> {
|
||||
match (*config).borrow().original_config.as_ref() {
|
||||
Some(v) => Arc::clone(v),
|
||||
None => Arc::clone(&config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConfiguredValue {
|
||||
pub value: Value,
|
||||
pub configuration_span: Option<Span>,
|
||||
}
|
||||
|
||||
impl ConfiguredValue {
|
||||
pub fn explicit(value: Value, configuration_span: Span) -> Self {
|
||||
Self {
|
||||
value,
|
||||
configuration_span: Some(configuration_span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstForwardRule {
|
||||
pub url: PathBuf,
|
||||
pub shown_mixins_and_functions: Option<HashSet<Identifier>>,
|
||||
pub shown_variables: Option<HashSet<Identifier>>,
|
||||
pub hidden_mixins_and_functions: Option<HashSet<Identifier>>,
|
||||
pub hidden_variables: Option<HashSet<Identifier>>,
|
||||
pub prefix: Option<String>,
|
||||
pub configuration: Vec<ConfiguredVariable>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl AstForwardRule {
|
||||
pub fn new(
|
||||
url: PathBuf,
|
||||
prefix: Option<String>,
|
||||
configuration: Option<Vec<ConfiguredVariable>>,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Self {
|
||||
url,
|
||||
shown_mixins_and_functions: None,
|
||||
shown_variables: None,
|
||||
hidden_mixins_and_functions: None,
|
||||
hidden_variables: None,
|
||||
prefix,
|
||||
configuration: configuration.unwrap_or_default(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
url: PathBuf,
|
||||
shown_mixins_and_functions: HashSet<Identifier>,
|
||||
shown_variables: HashSet<Identifier>,
|
||||
prefix: Option<String>,
|
||||
configuration: Option<Vec<ConfiguredVariable>>,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Self {
|
||||
url,
|
||||
shown_mixins_and_functions: Some(shown_mixins_and_functions),
|
||||
shown_variables: Some(shown_variables),
|
||||
hidden_mixins_and_functions: None,
|
||||
hidden_variables: None,
|
||||
prefix,
|
||||
configuration: configuration.unwrap_or_default(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide(
|
||||
url: PathBuf,
|
||||
hidden_mixins_and_functions: HashSet<Identifier>,
|
||||
hidden_variables: HashSet<Identifier>,
|
||||
prefix: Option<String>,
|
||||
configuration: Option<Vec<ConfiguredVariable>>,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Self {
|
||||
url,
|
||||
shown_mixins_and_functions: None,
|
||||
shown_variables: None,
|
||||
hidden_mixins_and_functions: Some(hidden_mixins_and_functions),
|
||||
hidden_variables: Some(hidden_variables),
|
||||
prefix,
|
||||
configuration: configuration.unwrap_or_default(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum AstSupportsCondition {
|
||||
Anything {
|
||||
contents: Interpolation,
|
||||
},
|
||||
Declaration {
|
||||
name: AstExpr,
|
||||
value: AstExpr,
|
||||
},
|
||||
Function {
|
||||
name: Interpolation,
|
||||
args: Interpolation,
|
||||
},
|
||||
Interpolation(AstExpr),
|
||||
Negation(Box<Self>),
|
||||
Operation {
|
||||
left: Box<Self>,
|
||||
operator: Option<String>,
|
||||
right: Box<Self>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AstSupportsRule {
|
||||
pub condition: AstSupportsCondition,
|
||||
pub children: Vec<AstStmt>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum AstStmt {
|
||||
If(AstIf),
|
||||
For(AstFor),
|
||||
Return(AstReturn),
|
||||
RuleSet(AstRuleSet),
|
||||
Style(AstStyle),
|
||||
Each(AstEach),
|
||||
Media(AstMedia),
|
||||
Include(AstInclude),
|
||||
While(AstWhile),
|
||||
VariableDecl(AstVariableDecl),
|
||||
LoudComment(AstLoudComment),
|
||||
SilentComment(AstSilentComment),
|
||||
FunctionDecl(AstFunctionDecl),
|
||||
Mixin(AstMixin),
|
||||
ContentRule(AstContentRule),
|
||||
Warn(AstWarn),
|
||||
UnknownAtRule(AstUnknownAtRule),
|
||||
ErrorRule(AstErrorRule),
|
||||
Extend(AstExtendRule),
|
||||
AtRootRule(AstAtRootRule),
|
||||
Debug(AstDebugRule),
|
||||
ImportRule(AstImportRule),
|
||||
Use(AstUseRule),
|
||||
Forward(AstForwardRule),
|
||||
Supports(AstSupportsRule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StyleSheet {
|
||||
pub body: Vec<AstStmt>,
|
||||
pub url: PathBuf,
|
||||
pub is_plain_css: bool,
|
||||
pub uses: Vec<AstUseRule>,
|
||||
pub forwards: Vec<AstForwardRule>,
|
||||
}
|
||||
|
||||
impl StyleSheet {
|
||||
pub fn new(is_plain_css: bool, url: PathBuf) -> Self {
|
||||
Self {
|
||||
body: Vec::new(),
|
||||
url,
|
||||
is_plain_css,
|
||||
uses: Vec::new(),
|
||||
forwards: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
11
src/ast/style.rs
Normal file
11
src/ast/style.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{interner::InternedString, value::Value};
|
||||
|
||||
/// A style: `color: red`
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Style {
|
||||
pub property: InternedString,
|
||||
pub value: Box<Spanned<Value>>,
|
||||
pub declared_as_custom_property: bool,
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
use crate::{parse::Stmt, selector::Selector};
|
||||
use crate::ast::CssStmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct UnknownAtRule {
|
||||
pub name: String,
|
||||
pub super_selector: Selector,
|
||||
// pub super_selector: Selector,
|
||||
pub params: String,
|
||||
pub body: Vec<Stmt>,
|
||||
pub body: Vec<CssStmt>,
|
||||
|
||||
/// Whether or not this @-rule was declared with curly
|
||||
/// braces. A body may not necessarily have contents
|
@ -1,38 +0,0 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use codemap::Span;
|
||||
|
||||
use crate::{args::FuncArgs, Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Function {
|
||||
pub args: FuncArgs,
|
||||
pub body: Vec<Token>,
|
||||
pub declared_at_root: bool,
|
||||
pos: Span,
|
||||
}
|
||||
|
||||
impl Hash for Function {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.pos.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Function {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.pos == other.pos
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Function {}
|
||||
|
||||
impl Function {
|
||||
pub fn new(args: FuncArgs, body: Vec<Token>, declared_at_root: bool, pos: Span) -> Self {
|
||||
Function {
|
||||
args,
|
||||
body,
|
||||
declared_at_root,
|
||||
pos,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use crate::parse::Stmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Keyframes {
|
||||
/// `@keyframes` can contain a browser prefix,
|
||||
/// e.g. `@-webkit-keyframes { ... }`, and therefore
|
||||
/// we cannot be certain of the name of the at-rule
|
||||
pub rule: String,
|
||||
pub name: String,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KeyframesRuleSet {
|
||||
pub selector: Vec<KeyframesSelector>,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum KeyframesSelector {
|
||||
To,
|
||||
From,
|
||||
Percent(Box<str>),
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{common::unvendor, error::SassError};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AtRuleKind {
|
||||
// Sass specific @rules
|
||||
/// Loads mixins, functions, and variables from other Sass
|
||||
/// stylesheets, and combines CSS from multiple stylesheets together
|
||||
Use,
|
||||
|
||||
/// Loads a Sass stylesheet and makes its mixins, functions,
|
||||
/// and variables available when your stylesheet is loaded
|
||||
/// with the `@use` rule
|
||||
Forward,
|
||||
|
||||
/// Extends the CSS at-rule to load styles, mixins, functions,
|
||||
/// and variables from other stylesheets
|
||||
///
|
||||
/// The definition inside `grass` however differs in that
|
||||
/// the @import rule refers to a plain css import
|
||||
/// e.g. `@import url(foo);`
|
||||
Import,
|
||||
|
||||
Mixin,
|
||||
Content,
|
||||
Include,
|
||||
|
||||
/// Defines custom functions that can be used in SassScript
|
||||
/// expressions
|
||||
Function,
|
||||
Return,
|
||||
|
||||
/// Allows selectors to inherit styles from one another
|
||||
Extend,
|
||||
|
||||
/// Puts styles within it at the root of the CSS document
|
||||
AtRoot,
|
||||
|
||||
/// Causes compilation to fail with an error message
|
||||
Error,
|
||||
|
||||
/// Prints a warning without stopping compilation entirely
|
||||
Warn,
|
||||
|
||||
/// Prints a message for debugging purposes
|
||||
Debug,
|
||||
|
||||
If,
|
||||
Each,
|
||||
For,
|
||||
While,
|
||||
|
||||
// CSS @rules
|
||||
/// Defines the character set used by the style sheet
|
||||
Charset,
|
||||
|
||||
/// A conditional group rule that will apply its content if the
|
||||
/// browser meets the criteria of the given condition
|
||||
Supports,
|
||||
|
||||
/// Describes the aspect of intermediate steps in a CSS animation sequence
|
||||
Keyframes,
|
||||
Media,
|
||||
|
||||
/// An unknown at-rule
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl TryFrom<&Spanned<String>> for AtRuleKind {
|
||||
type Error = Box<SassError>;
|
||||
fn try_from(c: &Spanned<String>) -> Result<Self, Box<SassError>> {
|
||||
match c.node.as_str() {
|
||||
"use" => return Ok(Self::Use),
|
||||
"forward" => return Ok(Self::Forward),
|
||||
"import" => return Ok(Self::Import),
|
||||
"mixin" => return Ok(Self::Mixin),
|
||||
"include" => return Ok(Self::Include),
|
||||
"function" => return Ok(Self::Function),
|
||||
"return" => return Ok(Self::Return),
|
||||
"extend" => return Ok(Self::Extend),
|
||||
"at-root" => return Ok(Self::AtRoot),
|
||||
"error" => return Ok(Self::Error),
|
||||
"warn" => return Ok(Self::Warn),
|
||||
"debug" => return Ok(Self::Debug),
|
||||
"if" => return Ok(Self::If),
|
||||
"each" => return Ok(Self::Each),
|
||||
"for" => return Ok(Self::For),
|
||||
"while" => return Ok(Self::While),
|
||||
"charset" => return Ok(Self::Charset),
|
||||
"supports" => return Ok(Self::Supports),
|
||||
"content" => return Ok(Self::Content),
|
||||
"media" => return Ok(Self::Media),
|
||||
"else" => return Err(("This at-rule is not allowed here.", c.span).into()),
|
||||
"" => return Err(("Expected identifier.", c.span).into()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(match unvendor(&c.node) {
|
||||
"keyframes" => Self::Keyframes,
|
||||
_ => Self::Unknown(c.node.clone()),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
args::{CallArgs, FuncArgs},
|
||||
error::SassResult,
|
||||
parse::{Parser, Stmt},
|
||||
Token,
|
||||
};
|
||||
|
||||
pub(crate) type BuiltinMixin = fn(CallArgs, &mut Parser) -> SassResult<Vec<Stmt>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Mixin {
|
||||
UserDefined(UserDefinedMixin),
|
||||
Builtin(BuiltinMixin),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Mixin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UserDefined(u) => f
|
||||
.debug_struct("UserDefinedMixin")
|
||||
.field("args", &u.args)
|
||||
.field("body", &u.body)
|
||||
.field("accepts_content_block", &u.accepts_content_block)
|
||||
.field("declared_at_root", &u.declared_at_root)
|
||||
.finish(),
|
||||
Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mixin {
|
||||
pub fn new_user_defined(
|
||||
args: FuncArgs,
|
||||
body: Vec<Token>,
|
||||
accepts_content_block: bool,
|
||||
declared_at_root: bool,
|
||||
) -> Self {
|
||||
Mixin::UserDefined(UserDefinedMixin::new(
|
||||
args,
|
||||
body,
|
||||
accepts_content_block,
|
||||
declared_at_root,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UserDefinedMixin {
|
||||
pub args: FuncArgs,
|
||||
pub body: Vec<Token>,
|
||||
pub accepts_content_block: bool,
|
||||
pub declared_at_root: bool,
|
||||
}
|
||||
|
||||
impl UserDefinedMixin {
|
||||
pub fn new(
|
||||
args: FuncArgs,
|
||||
body: Vec<Token>,
|
||||
accepts_content_block: bool,
|
||||
declared_at_root: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
args,
|
||||
body,
|
||||
accepts_content_block,
|
||||
declared_at_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Content {
|
||||
/// The literal block, serialized as a list of tokens
|
||||
pub content: Option<Vec<Token>>,
|
||||
|
||||
/// Optional args, e.g. `@content(a, b, c);`
|
||||
pub content_args: Option<FuncArgs>,
|
||||
|
||||
/// The number of scopes at the use of `@include`
|
||||
///
|
||||
/// This is used to "reset" back to the state of the `@include`
|
||||
/// without actually cloning the scope or putting it in an `Rc`
|
||||
pub scope_len: usize,
|
||||
|
||||
/// Whether or not the mixin this `@content` block is inside of was
|
||||
/// declared in the global scope
|
||||
pub declared_at_root: bool,
|
||||
}
|
@ -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;
|
@ -1,7 +0,0 @@
|
||||
use crate::parse::Stmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SupportsRule {
|
||||
pub params: String,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
@ -1,115 +1,28 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use codemap::Spanned;
|
||||
use num_traits::One;
|
||||
use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
color::Color,
|
||||
common::{Brackets, ListSeparator, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
use super::rgb::{function_string, parse_channels, percentage_or_unitless, ParsedChannels};
|
||||
|
||||
fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
if args.is_empty() {
|
||||
return Err(("Missing argument $channels.", args.span()).into());
|
||||
}
|
||||
fn hsl_3_args(
|
||||
name: &'static str,
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
let span = args.span();
|
||||
|
||||
let len = args.len();
|
||||
|
||||
if len == 1 {
|
||||
let mut channels = match args.get_err(0, "channels")? {
|
||||
Value::List(v, ..) => v,
|
||||
v if v.is_special_function() => vec![v],
|
||||
_ => return Err(("Missing argument $channels.", args.span()).into()),
|
||||
};
|
||||
|
||||
if channels.len() > 3 {
|
||||
return Err((
|
||||
format!(
|
||||
"Only 3 elements allowed, but {} were passed.",
|
||||
channels.len()
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if channels.iter().any(Value::is_special_function) {
|
||||
let channel_sep = if channels.len() < 3 {
|
||||
ListSeparator::Space
|
||||
} else {
|
||||
ListSeparator::Comma
|
||||
};
|
||||
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({})",
|
||||
name,
|
||||
Value::List(channels, channel_sep, Brackets::None)
|
||||
.to_css_string(args.span(), false)?
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
|
||||
let lightness = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$lightness: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $lightness.", args.span()).into()),
|
||||
};
|
||||
|
||||
let saturation = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$saturation: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $saturation.", args.span()).into()),
|
||||
};
|
||||
|
||||
let hue = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), ..)) => n,
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $hue.", args.span()).into()),
|
||||
};
|
||||
|
||||
Ok(Value::Color(Box::new(Color::from_hsla(
|
||||
hue,
|
||||
saturation,
|
||||
lightness,
|
||||
Number::one(),
|
||||
))))
|
||||
} else {
|
||||
let hue = args.get_err(0, "hue")?;
|
||||
let 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),
|
||||
)?;
|
||||
Value::Dimension(SassNumber {
|
||||
num: (Number::one()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
|
||||
if [&hue, &saturation, &lightness, &alpha]
|
||||
.iter()
|
||||
@ -121,7 +34,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
|
||||
"{}({})",
|
||||
name,
|
||||
Value::List(
|
||||
if len == 4 {
|
||||
if args.len() == 4 {
|
||||
vec![hue, saturation, lightness, alpha]
|
||||
} else {
|
||||
vec![hue, saturation, lightness]
|
||||
@ -135,85 +48,89 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
|
||||
));
|
||||
}
|
||||
|
||||
let hue = match hue {
|
||||
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,
|
||||
let hue = hue.assert_number_with_name("hue", span)?;
|
||||
let saturation = saturation.assert_number_with_name("saturation", span)?;
|
||||
let lightness = lightness.assert_number_with_name("lightness", span)?;
|
||||
let alpha = percentage_or_unitless(
|
||||
&alpha.assert_number_with_name("alpha", span)?,
|
||||
1.0,
|
||||
"alpha",
|
||||
span,
|
||||
visitor,
|
||||
)?;
|
||||
|
||||
Ok(Value::Color(Box::new(Color::from_hsla_fn(
|
||||
Number(hue.num().rem_euclid(360.0)),
|
||||
saturation.num() / Number::from(100),
|
||||
lightness.num() / Number::from(100),
|
||||
Number(alpha),
|
||||
))))
|
||||
}
|
||||
|
||||
fn inner_hsl(
|
||||
name: &'static str,
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(4)?;
|
||||
let span = args.span();
|
||||
|
||||
let len = args.len();
|
||||
|
||||
if len == 1 || len == 0 {
|
||||
match parse_channels(
|
||||
name,
|
||||
&["hue", "saturation", "lightness"],
|
||||
args.get_err(0, "channels")?,
|
||||
visitor,
|
||||
args.span(),
|
||||
)? {
|
||||
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
|
||||
ParsedChannels::List(list) => {
|
||||
let args = ArgumentResult {
|
||||
positional: list,
|
||||
named: BTreeMap::new(),
|
||||
separator: ListSeparator::Comma,
|
||||
span: args.span(),
|
||||
touched: BTreeSet::new(),
|
||||
};
|
||||
|
||||
hsl_3_args(name, args, visitor)
|
||||
}
|
||||
}
|
||||
} else if len == 2 {
|
||||
let hue = args.get_err(0, "hue")?;
|
||||
let saturation = args.get_err(1, "saturation")?;
|
||||
|
||||
if hue.is_var() || saturation.is_var() {
|
||||
return Ok(Value::String(
|
||||
function_string(name, &[hue, saturation], visitor, span)?,
|
||||
QuoteKind::None,
|
||||
));
|
||||
} else {
|
||||
return Err(("Missing argument $lightness.", args.span()).into());
|
||||
}
|
||||
} else {
|
||||
return hsl_3_args(name, args, visitor);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hsl(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
inner_hsl("hsl", args, parser)
|
||||
pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
inner_hsl("hsl", args, visitor)
|
||||
}
|
||||
|
||||
pub(crate) fn hsla(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
inner_hsl("hsla", args, parser)
|
||||
pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
inner_hsl("hsla", args, visitor)
|
||||
}
|
||||
|
||||
pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.hue()), Unit::Deg, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.hue()),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -222,10 +139,14 @@ pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.saturation()), Unit::Percent, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.saturation()),
|
||||
unit: Unit::Percent,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -234,10 +155,14 @@ pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.lightness()), Unit::Percent, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: c.lightness(),
|
||||
unit: Unit::Percent,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -246,36 +171,20 @@ pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let degrees = match args.get_err(1, "degrees")? {
|
||||
Value::Dimension(Some(n), ..) => n,
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"$degrees: {} is not a number.",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let color = args
|
||||
.get_err(0, "color")?
|
||||
.assert_color_with_name("color", args.span())?;
|
||||
let degrees = args
|
||||
.get_err(1, "degrees")?
|
||||
.assert_number_with_name("degrees", args.span())?
|
||||
.num();
|
||||
|
||||
Ok(Value::Color(Box::new(color.adjust_hue(degrees))))
|
||||
}
|
||||
|
||||
fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -287,24 +196,16 @@ fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"$amount: {} is not a number.",
|
||||
v.to_css_string(args.span(), false)?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let amount = args
|
||||
.get_err(1, "amount")?
|
||||
.assert_number_with_name("amount", args.span())?;
|
||||
let amount = bound!(args, "amount", amount.num(), amount.unit, 0, 100) / Number(100.0);
|
||||
|
||||
Ok(Value::Color(Box::new(color.lighten(amount))))
|
||||
}
|
||||
|
||||
fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -317,8 +218,12 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
@ -333,22 +238,29 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
Ok(Value::Color(Box::new(color.darken(amount))))
|
||||
}
|
||||
|
||||
fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
if args.len() == 1 {
|
||||
let amount = args
|
||||
.get_err(0, "amount")?
|
||||
.assert_number_with_name("amount", args.span())?;
|
||||
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"saturate({})",
|
||||
args.get_err(0, "amount")?
|
||||
.to_css_string(args.span(), false)?
|
||||
serialize_number(&amount, &Options::default(), args.span())?,
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
@ -362,11 +274,16 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
Value::Dimension(Some(n), u, _) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
// todo: this branch should be superfluous/incorrect
|
||||
return Ok(Value::String(
|
||||
format!("saturate({}{})", n.inspect(), u),
|
||||
QuoteKind::None,
|
||||
))
|
||||
));
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
@ -379,7 +296,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
Ok(Value::Color(Box::new(color.saturate(amount))))
|
||||
}
|
||||
|
||||
fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -392,13 +309,17 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"$amount: {} is not a number.",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
@ -408,11 +329,15 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
Ok(Value::Color(Box::new(color.desaturate(amount))))
|
||||
}
|
||||
|
||||
pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
Value::Dimension(Some(n), u, _) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
return Ok(Value::String(
|
||||
format!("grayscale({}{})", n.inspect(), u),
|
||||
QuoteKind::None,
|
||||
@ -429,7 +354,7 @@ pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
Ok(Value::Color(Box::new(color.desaturate(Number::one()))))
|
||||
}
|
||||
|
||||
pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -444,24 +369,28 @@ pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
Ok(Value::Color(Box::new(color.complement())))
|
||||
}
|
||||
|
||||
pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let weight = match args.get(1, "weight") {
|
||||
Some(Err(e)) => return Err(e),
|
||||
Some(Ok(Spanned {
|
||||
node: Value::Dimension(Some(n), u, _),
|
||||
Some(Spanned {
|
||||
node: Value::Dimension(SassNumber { num: n, .. }),
|
||||
..
|
||||
})) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
|
||||
Some(Ok(Spanned {
|
||||
node: Value::Dimension(None, ..),
|
||||
}) if n.is_nan() => todo!(),
|
||||
Some(Spanned {
|
||||
node:
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}),
|
||||
..
|
||||
})) => todo!(),
|
||||
}) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
|
||||
None => None,
|
||||
Some(Ok(v)) => {
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$weight: {} is not a number.",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
@ -472,7 +401,11 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
||||
Value::Color(c) => Ok(Value::Color(Box::new(
|
||||
c.invert(weight.unwrap_or_else(Number::one)),
|
||||
))),
|
||||
Value::Dimension(Some(n), u, _) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
if weight.is_some() {
|
||||
return Err((
|
||||
"Only one argument may be passed to the plain-CSS invert() function.",
|
||||
@ -485,9 +418,6 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
||||
QuoteKind::None,
|
||||
))
|
||||
}
|
||||
Value::Dimension(None, u, _) => {
|
||||
Ok(Value::String(format!("invert(NaN{})", u), QuoteKind::None))
|
||||
}
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
|
@ -1,15 +1,8 @@
|
||||
use num_traits::One;
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
color::Color,
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
use super::rgb::{parse_channels, ParsedChannels};
|
||||
|
||||
pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let color = match args.get_err(0, "color")? {
|
||||
@ -26,39 +19,36 @@ pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
let blackness =
|
||||
Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255));
|
||||
|
||||
Ok(Value::Dimension(Some(blackness * 100), Unit::Percent, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (blackness * 100),
|
||||
unit: Unit::Percent,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn whiteness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let color = args
|
||||
.get_err(0, "color")?
|
||||
.assert_color_with_name("color", args.span())?;
|
||||
|
||||
let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255);
|
||||
|
||||
Ok(Value::Dimension(Some(whiteness * 100), Unit::Percent, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (whiteness * 100),
|
||||
unit: Unit::Percent,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
args.max_args(4)?;
|
||||
|
||||
if args.is_empty() {
|
||||
return Err(("Missing argument $channels.", args.span()).into());
|
||||
}
|
||||
fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let span = args.span();
|
||||
|
||||
let hue = match args.get(0, "hue") {
|
||||
Some(Ok(v)) => match v.node {
|
||||
Value::Dimension(Some(n), ..) => n,
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Some(v) => match v.node {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) => n,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -67,57 +57,28 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => return Err(("Missing element $hue.", args.span()).into()),
|
||||
};
|
||||
|
||||
let whiteness = match args.get(1, "whiteness") {
|
||||
Some(Ok(v)) => match v.node {
|
||||
Value::Dimension(Some(n), Unit::Percent, ..) => n,
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$whiteness: Expected {} to have unit \"%\".",
|
||||
v.inspect(args.span())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$whiteness: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => return Err(("Missing element $whiteness.", args.span()).into()),
|
||||
};
|
||||
let whiteness = args
|
||||
.get_err(1, "whiteness")?
|
||||
.assert_number_with_name("whiteness", span)?;
|
||||
whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
|
||||
|
||||
let blackness = match args.get(2, "blackness") {
|
||||
Some(Ok(v)) => match v.node {
|
||||
Value::Dimension(Some(n), ..) => n,
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$blackness: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => return Err(("Missing element $blackness.", args.span()).into()),
|
||||
};
|
||||
let blackness = args
|
||||
.get_err(2, "blackness")?
|
||||
.assert_number_with_name("blackness", span)?;
|
||||
blackness.assert_unit(&Unit::Percent, "blackness", span)?;
|
||||
|
||||
let alpha = match args.get(3, "alpha") {
|
||||
Some(Ok(v)) => match v.node {
|
||||
Value::Dimension(Some(n), Unit::Percent, ..) => n / Number::from(100),
|
||||
Value::Dimension(Some(n), ..) => n,
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Some(v) => match v.node {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: Unit::Percent,
|
||||
..
|
||||
}) => n / Number::from(100),
|
||||
Value::Dimension(SassNumber { num: n, .. }) => n,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -126,11 +87,44 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => Number::one(),
|
||||
};
|
||||
|
||||
Ok(Value::Color(Box::new(Color::from_hwb(
|
||||
hue, whiteness, blackness, alpha,
|
||||
hue,
|
||||
whiteness.num,
|
||||
blackness.num,
|
||||
alpha,
|
||||
))))
|
||||
}
|
||||
|
||||
pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(4)?;
|
||||
|
||||
if args.len() == 0 || args.len() == 1 {
|
||||
match parse_channels(
|
||||
"hwb",
|
||||
&["hue", "whiteness", "blackness"],
|
||||
args.get_err(0, "channels")?,
|
||||
visitor,
|
||||
args.span(),
|
||||
)? {
|
||||
ParsedChannels::String(s) => {
|
||||
Err((format!("Expected numeric channels, got {}", s), args.span()).into())
|
||||
}
|
||||
ParsedChannels::List(list) => {
|
||||
let args = ArgumentResult {
|
||||
positional: list,
|
||||
named: BTreeMap::new(),
|
||||
separator: ListSeparator::Comma,
|
||||
span: args.span(),
|
||||
touched: BTreeSet::new(),
|
||||
};
|
||||
|
||||
hwb_inner(args, visitor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hwb_inner(args, visitor)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use super::GlobalFunctionMap;
|
||||
|
||||
pub mod hsl;
|
||||
pub mod hwb;
|
||||
|
@ -1,9 +1,4 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs, common::QuoteKind, error::SassResult, parse::Parser, unit::Unit, value::Number,
|
||||
value::Value,
|
||||
};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
|
||||
fn is_ms_filter(s: &str) -> bool {
|
||||
@ -35,10 +30,14 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
if args.len() <= 1 {
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.alpha()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
|
||||
Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
|
||||
}
|
||||
@ -69,15 +68,23 @@ pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
|
||||
Value::Dimension(Some(num), unit, _) => Ok(Value::String(
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.alpha()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => Ok(Value::String(
|
||||
format!("opacity({}{})", num.inspect(), unit),
|
||||
QuoteKind::None,
|
||||
)),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -86,8 +93,7 @@ pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
}
|
||||
}
|
||||
|
||||
// todo: unify `opacify` and `fade_in`
|
||||
fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -99,21 +105,16 @@ fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let amount = args
|
||||
.get_err(1, "amount")?
|
||||
.assert_number_with_name("amount", args.span())?;
|
||||
|
||||
let amount = bound!(args, "amount", amount.num(), amount.unit(), 0, 1);
|
||||
|
||||
Ok(Value::Color(Box::new(color.fade_in(amount))))
|
||||
}
|
||||
|
||||
fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
@ -126,61 +127,12 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
Ok(Value::Color(Box::new(color.fade_in(amount))))
|
||||
}
|
||||
|
||||
// todo: unify with `fade_out`
|
||||
fn transparentize(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
Ok(Value::Color(Box::new(color.fade_out(amount))))
|
||||
}
|
||||
|
||||
fn fade_out(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let amount = match args.get_err(1, "amount")? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => bound!(args, "amount", n, u, 0, 1),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -196,7 +148,7 @@ pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||
f.insert("alpha", Builtin::new(alpha));
|
||||
f.insert("opacity", Builtin::new(opacity));
|
||||
f.insert("opacify", Builtin::new(opacify));
|
||||
f.insert("fade-in", Builtin::new(fade_in));
|
||||
f.insert("fade-in", Builtin::new(opacify));
|
||||
f.insert("transparentize", Builtin::new(transparentize));
|
||||
f.insert("fade-out", Builtin::new(fade_out));
|
||||
f.insert("fade-out", Builtin::new(transparentize));
|
||||
}
|
||||
|
@ -1,22 +1,12 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
|
||||
use num_traits::{One, Signed, Zero};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
color::Color,
|
||||
common::QuoteKind,
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
macro_rules! opt_rgba {
|
||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
||||
Value::Dimension(Some(n), u, _) => Some(bound!($args, $arg, n, u, $low, $high)),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) => Some(bound!($args, $arg, n, u, $low, $high)),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
return Err((
|
||||
@ -31,11 +21,11 @@ macro_rules! opt_rgba {
|
||||
|
||||
macro_rules! opt_hsl {
|
||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
||||
Value::Dimension(Some(n), u, _) => {
|
||||
Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100))
|
||||
}
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) => Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
return Err((
|
||||
@ -48,8 +38,8 @@ macro_rules! opt_hsl {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
if args.positional_arg(1).is_some() {
|
||||
pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
if args.get_positional(1).is_some() {
|
||||
return Err((
|
||||
"Only one positional argument is allowed. All other arguments must be passed by name.",
|
||||
args.span(),
|
||||
@ -82,9 +72,9 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
))));
|
||||
}
|
||||
|
||||
let hue = match args.default_named_arg("hue", Value::Null)? {
|
||||
Value::Dimension(Some(n), ..) => Some(n),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
let hue = match args.default_named_arg("hue", Value::Null) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
return Err((
|
||||
@ -116,7 +106,7 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
@ -142,9 +132,9 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
))));
|
||||
}
|
||||
|
||||
let hue = match args.default_named_arg("hue", Value::Null)? {
|
||||
Value::Dimension(Some(n), ..) => Some(n),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
let hue = match args.default_named_arg("hue", Value::Null) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
return Err((
|
||||
@ -179,12 +169,12 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
// todo: refactor into rgb and hsl?
|
||||
pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
|
||||
if by.is_zero() {
|
||||
return val;
|
||||
}
|
||||
val.clone() + (if by.is_positive() { max - val } else { val }) * by
|
||||
val + (if by.is_positive() { max - val } else { val }) * by
|
||||
}
|
||||
|
||||
let span = args.span();
|
||||
@ -201,12 +191,14 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
|
||||
macro_rules! opt_scale_arg {
|
||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
||||
Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100))
|
||||
}
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v @ Value::Dimension(..) => {
|
||||
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: Unit::Percent,
|
||||
..
|
||||
}) => Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)),
|
||||
v @ Value::Dimension { .. } => {
|
||||
return Err((
|
||||
format!(
|
||||
"${}: Expected {} to have unit \"%\".",
|
||||
@ -293,7 +285,7 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn ie_hex_str(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let color = match args.get_err(0, "color")? {
|
||||
Value::Color(c) => c,
|
||||
|
@ -1,368 +1,365 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::{builtin::builtin_imports::*, serializer::inspect_number, value::fuzzy_round};
|
||||
|
||||
use num_traits::One;
|
||||
pub(crate) fn function_string(
|
||||
name: &'static str,
|
||||
args: &[Value],
|
||||
visitor: &mut Visitor,
|
||||
span: Span,
|
||||
) -> SassResult<String> {
|
||||
let args = args
|
||||
.iter()
|
||||
.map(|arg| arg.to_css_string(span, visitor.options.is_compressed()))
|
||||
.collect::<SassResult<Vec<_>>>()?
|
||||
.join(", ");
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
color::Color,
|
||||
common::{Brackets, ListSeparator, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
|
||||
/// name: Either `rgb` or `rgba` depending on the caller
|
||||
// todo: refactor into smaller functions
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
if args.is_empty() {
|
||||
return Err(("Missing argument $channels.", args.span()).into());
|
||||
Ok(format!("{}({})", name, args))
|
||||
}
|
||||
|
||||
let len = args.len();
|
||||
|
||||
if len == 1 {
|
||||
let mut channels = match args.get_err(0, "channels")? {
|
||||
Value::List(v, ..) => v,
|
||||
v if v.is_special_function() => vec![v],
|
||||
_ => return Err(("Missing argument $channels.", args.span()).into()),
|
||||
};
|
||||
|
||||
if channels.len() > 3 {
|
||||
return Err((
|
||||
format!(
|
||||
"Only 3 elements allowed, but {} were passed.",
|
||||
channels.len()
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if channels.iter().any(Value::is_special_function) {
|
||||
let channel_sep = if channels.len() < 3 {
|
||||
ListSeparator::Space
|
||||
} else {
|
||||
ListSeparator::Comma
|
||||
};
|
||||
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({})",
|
||||
name,
|
||||
Value::List(channels, channel_sep, Brackets::None)
|
||||
.to_css_string(args.span(), false)?
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
|
||||
let blue = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
||||
(n / Number::from(100)) * Number::from(255)
|
||||
}
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) if v.is_special_function() => {
|
||||
let green = channels.pop().unwrap();
|
||||
let red = channels.pop().unwrap();
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({}, {}, {})",
|
||||
name,
|
||||
red.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
green.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$blue: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $blue.", args.span()).into()),
|
||||
};
|
||||
|
||||
let green = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
||||
(n / Number::from(100)) * Number::from(255)
|
||||
}
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) if v.is_special_function() => {
|
||||
let string = match channels.pop() {
|
||||
Some(red) => format!(
|
||||
"{}({}, {}, {})",
|
||||
name,
|
||||
red.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
blue.to_string(parser.options.is_compressed())
|
||||
),
|
||||
None => format!(
|
||||
"{}({} {})",
|
||||
name,
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
blue.to_string(parser.options.is_compressed())
|
||||
),
|
||||
};
|
||||
return Ok(Value::String(string, QuoteKind::None));
|
||||
}
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$green: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $green.", args.span()).into()),
|
||||
};
|
||||
|
||||
let red = match channels.pop() {
|
||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
||||
(n / Number::from(100)) * Number::from(255)
|
||||
}
|
||||
Some(Value::Dimension(None, ..)) => todo!(),
|
||||
Some(v) if v.is_special_function() => {
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({}, {}, {})",
|
||||
name,
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
||||
green.to_string(parser.options.is_compressed()),
|
||||
blue.to_string(parser.options.is_compressed())
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
Some(v) => {
|
||||
return Err((
|
||||
format!("$red: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
None => return Err(("Missing element $red.", args.span()).into()),
|
||||
};
|
||||
|
||||
let color = Color::from_rgba(red, green, blue, Number::one());
|
||||
|
||||
Ok(Value::Color(Box::new(color)))
|
||||
} else if len == 2 {
|
||||
fn inner_rgb_2_arg(
|
||||
name: &'static str,
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
|
||||
// and functions are parsed after variable substitution.
|
||||
let color = args.get_err(0, "color")?;
|
||||
let alpha = args.get_err(1, "alpha")?;
|
||||
|
||||
if color.is_special_function() || (alpha.is_special_function() && !color.is_color()) {
|
||||
let is_compressed = visitor.options.is_compressed();
|
||||
|
||||
if color.is_var() {
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({})",
|
||||
name,
|
||||
Value::List(vec![color, alpha], ListSeparator::Comma, Brackets::None)
|
||||
.to_css_string(args.span(), false)?
|
||||
),
|
||||
function_string(name, &[color, alpha], visitor, args.span())?,
|
||||
QuoteKind::None,
|
||||
));
|
||||
}
|
||||
|
||||
let color = match color {
|
||||
Value::Color(c) => c,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
if alpha.is_special_function() {
|
||||
} else if alpha.is_var() {
|
||||
match &color {
|
||||
Value::Color(color) => {
|
||||
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)?,
|
||||
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 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())
|
||||
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)?,
|
||||
)))))
|
||||
}
|
||||
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))))
|
||||
|
||||
fn inner_rgb_3_arg(
|
||||
name: &'static str,
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
let alpha = if args.len() > 3 {
|
||||
args.get(3, "alpha")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let red = args.get_err(0, "red")?;
|
||||
let green = args.get_err(1, "green")?;
|
||||
let blue = args.get_err(2, "blue")?;
|
||||
let alpha = args.default_arg(
|
||||
3,
|
||||
"alpha",
|
||||
Value::Dimension(Some(Number::one()), Unit::None, true),
|
||||
)?;
|
||||
|
||||
if [&red, &green, &blue, &alpha]
|
||||
.iter()
|
||||
.copied()
|
||||
.any(Value::is_special_function)
|
||||
if 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)
|
||||
{
|
||||
return Ok(Value::String(
|
||||
format!(
|
||||
"{}({})",
|
||||
let fn_string = if alpha.is_some() {
|
||||
function_string(
|
||||
name,
|
||||
Value::List(
|
||||
if len == 4 {
|
||||
vec![red, green, blue, alpha]
|
||||
&[red, green, blue, alpha.unwrap().node],
|
||||
visitor,
|
||||
args.span(),
|
||||
)?
|
||||
} else {
|
||||
vec![red, green, blue]
|
||||
},
|
||||
ListSeparator::Comma,
|
||||
Brackets::None
|
||||
)
|
||||
.to_css_string(args.span(), false)?
|
||||
),
|
||||
QuoteKind::None,
|
||||
));
|
||||
function_string(name, &[red, green, blue], visitor, args.span())?
|
||||
};
|
||||
|
||||
return Ok(Value::String(fn_string, QuoteKind::None));
|
||||
}
|
||||
|
||||
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())?
|
||||
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),
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$red: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let green = match green {
|
||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
||||
(n / Number::from(100)) * Number::from(255)
|
||||
}
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$green: Expected {} to have no units or \"%\".",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$green: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let blue = match blue {
|
||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
||||
(n / Number::from(100)) * Number::from(255)
|
||||
}
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$blue: Expected {} to have no units or \"%\".",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$blue: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let alpha = match alpha {
|
||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
||||
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$alpha: Expected {} to have no units or \"%\".",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
Ok(Value::Color(Box::new(Color::from_rgba(
|
||||
red, green, blue, alpha,
|
||||
))))
|
||||
}
|
||||
|
||||
pub(crate) fn percentage_or_unitless(
|
||||
number: &SassNumber,
|
||||
max: f64,
|
||||
name: &str,
|
||||
span: Span,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<f64> {
|
||||
let value = if number.unit == Unit::None {
|
||||
number.num
|
||||
} else if number.unit == Unit::Percent {
|
||||
(number.num * Number(max)) / Number(100.0)
|
||||
} else {
|
||||
return Err((
|
||||
format!(
|
||||
"${name}: Expected {} to have no units or \"%\".",
|
||||
inspect_number(number, visitor.options, span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
Ok(value.clamp(0.0, max).0)
|
||||
}
|
||||
|
||||
pub(crate) fn rgb(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
inner_rgb("rgb", args, parser)
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum ParsedChannels {
|
||||
String(String),
|
||||
List(Vec<Value>),
|
||||
}
|
||||
|
||||
pub(crate) fn rgba(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
inner_rgb("rgba", args, parser)
|
||||
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 red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn parse_channels(
|
||||
name: &'static str,
|
||||
arg_names: &[&'static str],
|
||||
mut channels: Value,
|
||||
visitor: &mut Visitor,
|
||||
span: Span,
|
||||
) -> SassResult<ParsedChannels> {
|
||||
if channels.is_var() {
|
||||
let fn_string = function_string(name, &[channels], visitor, span)?;
|
||||
return Ok(ParsedChannels::String(fn_string));
|
||||
}
|
||||
|
||||
let original_channels = channels.clone();
|
||||
|
||||
let mut alpha_from_slash_list = None;
|
||||
|
||||
if channels.separator() == ListSeparator::Slash {
|
||||
let list = channels.clone().as_list();
|
||||
if list.len() != 2 {
|
||||
return Err((
|
||||
format!(
|
||||
"Only 2 slash-separated elements allowed, but {} {} passed.",
|
||||
list.len(),
|
||||
if list.len() == 1 { "was" } else { "were" }
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
channels = list[0].clone();
|
||||
let inner_alpha_from_slash_list = list[1].clone();
|
||||
|
||||
if !alpha_from_slash_list
|
||||
.as_ref()
|
||||
.map(Value::is_special_function)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
inner_alpha_from_slash_list
|
||||
.clone()
|
||||
.assert_number_with_name("alpha", span)?;
|
||||
}
|
||||
|
||||
alpha_from_slash_list = Some(inner_alpha_from_slash_list);
|
||||
|
||||
if list[0].is_var() {
|
||||
let fn_string = function_string(name, &[original_channels], visitor, span)?;
|
||||
return Ok(ParsedChannels::String(fn_string));
|
||||
}
|
||||
}
|
||||
|
||||
let is_comma_separated = channels.separator() == ListSeparator::Comma;
|
||||
let is_bracketed = matches!(channels, Value::List(_, _, Brackets::Bracketed));
|
||||
|
||||
if is_comma_separated || is_bracketed {
|
||||
let mut err_buffer = "$channels must be".to_owned();
|
||||
|
||||
if is_bracketed {
|
||||
err_buffer.push_str(" an unbracketed");
|
||||
}
|
||||
|
||||
if is_comma_separated {
|
||||
if is_bracketed {
|
||||
err_buffer.push(',');
|
||||
} else {
|
||||
err_buffer.push_str(" a");
|
||||
}
|
||||
|
||||
err_buffer.push_str(" space-separated");
|
||||
}
|
||||
|
||||
err_buffer.push_str(" list.");
|
||||
|
||||
return Err((err_buffer, span).into());
|
||||
}
|
||||
|
||||
let mut list = channels.clone().as_list();
|
||||
|
||||
if list.len() > 3 {
|
||||
return Err((
|
||||
format!("Only 3 elements allowed, but {} were passed.", list.len()),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
} else if list.len() < 3 {
|
||||
if list.iter().any(Value::is_var)
|
||||
|| (!list.is_empty() && is_var_slash(list.last().unwrap()))
|
||||
{
|
||||
let fn_string = function_string(name, &[original_channels], visitor, span)?;
|
||||
return Ok(ParsedChannels::String(fn_string));
|
||||
} else {
|
||||
let argument = arg_names[list.len()];
|
||||
return Err((format!("Missing element ${argument}."), span).into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(alpha_from_slash_list) = alpha_from_slash_list {
|
||||
list.push(alpha_from_slash_list);
|
||||
return Ok(ParsedChannels::List(list));
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_match)]
|
||||
match &list[2] {
|
||||
Value::Dimension(SassNumber { as_slash, .. }) => match as_slash {
|
||||
Some(slash) => Ok(ParsedChannels::List(vec![
|
||||
list[0].clone(),
|
||||
list[1].clone(),
|
||||
// todo: superfluous clones
|
||||
Value::Dimension(slash.0.clone()),
|
||||
Value::Dimension(slash.1.clone()),
|
||||
])),
|
||||
None => Ok(ParsedChannels::List(list)),
|
||||
},
|
||||
Value::String(text, QuoteKind::None) if text.contains('/') => {
|
||||
let fn_string = function_string(name, &[channels], visitor, span)?;
|
||||
Ok(ParsedChannels::String(fn_string))
|
||||
}
|
||||
_ => Ok(ParsedChannels::List(list)),
|
||||
}
|
||||
}
|
||||
|
||||
/// name: Either `rgb` or `rgba` depending on the caller
|
||||
fn inner_rgb(
|
||||
name: &'static str,
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(4)?;
|
||||
|
||||
match args.len() {
|
||||
0 | 1 => {
|
||||
match parse_channels(
|
||||
name,
|
||||
&["red", "green", "blue"],
|
||||
args.get_err(0, "channels")?,
|
||||
visitor,
|
||||
args.span(),
|
||||
)? {
|
||||
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
|
||||
ParsedChannels::List(list) => {
|
||||
let args = ArgumentResult {
|
||||
positional: list,
|
||||
named: BTreeMap::new(),
|
||||
separator: ListSeparator::Comma,
|
||||
span: args.span(),
|
||||
touched: BTreeSet::new(),
|
||||
};
|
||||
|
||||
inner_rgb_3_arg(name, args, visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => inner_rgb_2_arg(name, args, visitor),
|
||||
_ => inner_rgb_3_arg(name, args, visitor),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
inner_rgb("rgb", args, visitor)
|
||||
}
|
||||
|
||||
pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
inner_rgb("rgba", args, visitor)
|
||||
}
|
||||
|
||||
pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.red()), Unit::None, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.red()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -371,10 +368,14 @@ pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.green()), Unit::None, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.green()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -383,10 +384,14 @@ pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "color")? {
|
||||
Value::Color(c) => Ok(Value::Dimension(Some(c.blue()), Unit::None, true)),
|
||||
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||
num: (c.blue()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -395,7 +400,7 @@ pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let color1 = match args.get_err(0, "color1")? {
|
||||
Value::Color(c) => c,
|
||||
@ -422,15 +427,23 @@ pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
let weight = match args.default_arg(
|
||||
2,
|
||||
"weight",
|
||||
Value::Dimension(Some(Number::from(50)), Unit::None, true),
|
||||
)? {
|
||||
Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
|
||||
Value::Dimension(None, ..) => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: (Number::from(50)),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
) {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"$weight: {} is not a number.",
|
||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
||||
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
|
@ -1,33 +1,24 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use num_traits::{Signed, ToPrimitive, Zero};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::{Brackets, ListSeparator, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
|
||||
pub(crate) fn length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(Value::Dimension(
|
||||
Some(Number::from(args.get_err(0, "list")?.as_list().len())),
|
||||
Unit::None,
|
||||
true,
|
||||
))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::from(args.get_err(0, "list")?.as_list().len())),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let mut list = args.get_err(0, "list")?.as_list();
|
||||
let (n, unit) = match args.get_err(1, "n")? {
|
||||
Value::Dimension(Some(num), unit, ..) => (num, unit),
|
||||
Value::Dimension(None, u, ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) if n.is_nan() => {
|
||||
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
||||
}
|
||||
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -54,18 +45,16 @@ pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
.into());
|
||||
}
|
||||
|
||||
if n.is_decimal() {
|
||||
return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
|
||||
}
|
||||
|
||||
Ok(list.remove(if n.is_positive() {
|
||||
n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1
|
||||
let index = n.assert_int_with_name("n", args.span())? - 1;
|
||||
debug_assert!(index > -1);
|
||||
index as usize
|
||||
} else {
|
||||
list.len() - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX)
|
||||
list.len() - n.abs().assert_int_with_name("n", args.span())? as usize
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(Value::String(
|
||||
match args.get_err(0, "list")? {
|
||||
@ -78,12 +67,12 @@ pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
||||
Value::List(v, sep, b) => (v, sep, b),
|
||||
Value::ArgList(v) => (
|
||||
v.into_iter().map(|val| val.node).collect(),
|
||||
v.elems.into_iter().collect(),
|
||||
ListSeparator::Comma,
|
||||
Brackets::None,
|
||||
),
|
||||
@ -91,10 +80,12 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
v => (vec![v], ListSeparator::Space, Brackets::None),
|
||||
};
|
||||
let (n, unit) = match args.get_err(1, "n")? {
|
||||
Value::Dimension(Some(num), unit, ..) => (num, unit),
|
||||
Value::Dimension(None, u, ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) if n.is_nan() => {
|
||||
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
||||
}
|
||||
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -130,15 +121,15 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
let val = args.get_err(2, "value")?;
|
||||
|
||||
if n.is_positive() {
|
||||
list[n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1] = val;
|
||||
list[n.assert_int_with_name("n", args.span())? as usize - 1] = val;
|
||||
} else {
|
||||
list[len - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX)] = val;
|
||||
list[len - n.abs().assert_int_with_name("n", args.span())? as usize] = val;
|
||||
}
|
||||
|
||||
Ok(Value::List(list, sep, brackets))
|
||||
}
|
||||
|
||||
pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
||||
Value::List(v, sep, b) => (v, sep, b),
|
||||
@ -149,14 +140,15 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
||||
2,
|
||||
"separator",
|
||||
Value::String("auto".to_owned(), QuoteKind::None),
|
||||
)? {
|
||||
) {
|
||||
Value::String(s, ..) => match s.as_str() {
|
||||
"auto" => sep,
|
||||
"comma" => ListSeparator::Comma,
|
||||
"space" => ListSeparator::Space,
|
||||
"slash" => ListSeparator::Slash,
|
||||
_ => {
|
||||
return Err((
|
||||
"$separator: Must be \"space\", \"comma\", or \"auto\".",
|
||||
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
@ -176,7 +168,7 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
||||
Ok(Value::List(list, sep, brackets))
|
||||
}
|
||||
|
||||
pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(4)?;
|
||||
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
|
||||
Value::List(v, sep, brackets) => (v, sep, brackets),
|
||||
@ -192,7 +184,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
2,
|
||||
"separator",
|
||||
Value::String("auto".to_owned(), QuoteKind::None),
|
||||
)? {
|
||||
) {
|
||||
Value::String(s, ..) => match s.as_str() {
|
||||
"auto" => {
|
||||
if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) {
|
||||
@ -203,9 +195,10 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
"comma" => ListSeparator::Comma,
|
||||
"space" => ListSeparator::Space,
|
||||
"slash" => ListSeparator::Slash,
|
||||
_ => {
|
||||
return Err((
|
||||
"$separator: Must be \"space\", \"comma\", or \"auto\".",
|
||||
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
@ -224,7 +217,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
3,
|
||||
"bracketed",
|
||||
Value::String("auto".to_owned(), QuoteKind::None),
|
||||
)? {
|
||||
) {
|
||||
Value::String(s, ..) => match s.as_str() {
|
||||
"auto" => brackets,
|
||||
_ => Brackets::Bracketed,
|
||||
@ -243,7 +236,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
Ok(Value::List(list1, sep, brackets))
|
||||
}
|
||||
|
||||
pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(Value::bool(match args.get_err(0, "list")? {
|
||||
Value::List(.., brackets) => match brackets {
|
||||
@ -254,7 +247,7 @@ pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let list = args.get_err(0, "list")?.as_list();
|
||||
let value = args.get_err(1, "value")?;
|
||||
@ -262,10 +255,14 @@ pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
Some(v) => Number::from(v + 1),
|
||||
None => return Ok(Value::Null),
|
||||
};
|
||||
Ok(Value::Dimension(Some(index), Unit::None, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (index),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn zip(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let lists = args
|
||||
.get_variadic()?
|
||||
.into_iter()
|
||||
|
@ -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 {}{}.",
|
||||
|
@ -1,14 +1,6 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::{Brackets, ListSeparator},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
value::{SassMap, Value},
|
||||
};
|
||||
|
||||
pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let key = args.get_err(1, "key")?;
|
||||
let map = match args.get_err(0, "map")? {
|
||||
@ -26,7 +18,7 @@ pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
Ok(map.get(&key).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let key = args.get_err(1, "key")?;
|
||||
let map = match args.get_err(0, "map")? {
|
||||
@ -44,7 +36,7 @@ pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
Ok(Value::bool(map.get(&key).is_some()))
|
||||
}
|
||||
|
||||
pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let map = match args.get_err(0, "map")? {
|
||||
Value::Map(m) => m,
|
||||
@ -65,7 +57,7 @@ pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let map = match args.get_err(0, "map")? {
|
||||
Value::Map(m) => m,
|
||||
@ -86,7 +78,7 @@ pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
if args.len() == 1 {
|
||||
return Err(("Expected $args to contain a key.", args.span()).into());
|
||||
}
|
||||
@ -150,10 +142,10 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
while let Some((key, queued_map)) = map_queue.pop() {
|
||||
match map_queue.last_mut() {
|
||||
Some((_, map)) => {
|
||||
map.insert(key.node, Value::Map(queued_map));
|
||||
map.insert(key, Value::Map(queued_map));
|
||||
}
|
||||
None => {
|
||||
map1.insert(key.node, Value::Map(queued_map));
|
||||
map1.insert(key, Value::Map(queued_map));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -163,7 +155,7 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
Ok(Value::Map(map1))
|
||||
}
|
||||
|
||||
pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let mut map = match args.get_err(0, "map")? {
|
||||
Value::Map(m) => m,
|
||||
Value::List(v, ..) if v.is_empty() => SassMap::new(),
|
||||
@ -183,7 +175,7 @@ pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
Ok(Value::Map(map))
|
||||
}
|
||||
|
||||
pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let key_position = args.len().saturating_sub(2);
|
||||
let value_position = args.len().saturating_sub(1);
|
||||
|
||||
@ -200,7 +192,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
}
|
||||
};
|
||||
|
||||
let key = args.get_err(key_position, "key")?;
|
||||
let key = Spanned {
|
||||
node: args.get_err(key_position, "key")?,
|
||||
span: args.span(),
|
||||
};
|
||||
let value = args.get_err(value_position, "value")?;
|
||||
|
||||
let keys = args.get_variadic()?;
|
||||
@ -232,10 +227,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
while let Some((key, queued_map)) = map_queue.pop() {
|
||||
match map_queue.last_mut() {
|
||||
Some((_, next_map)) => {
|
||||
next_map.insert(key.node, Value::Map(queued_map));
|
||||
next_map.insert(key, Value::Map(queued_map));
|
||||
}
|
||||
None => {
|
||||
map.insert(key.node, Value::Map(queued_map));
|
||||
map.insert(key, Value::Map(queued_map));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,14 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::{builtin::builtin_imports::*, evaluate::div};
|
||||
|
||||
#[cfg(feature = "random")]
|
||||
use num_traits::{One, Signed, ToPrimitive, Zero};
|
||||
#[cfg(feature = "random")]
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::Op,
|
||||
error::SassResult,
|
||||
parse::{HigherIntermediateValue, Parser, ValueVisitor},
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
|
||||
pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let num = match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), Unit::None, _) => Some(n * Number::from(100)),
|
||||
Value::Dimension(None, Unit::None, _) => None,
|
||||
v @ Value::Dimension(..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: Unit::None,
|
||||
as_slash: _,
|
||||
}) => n * Number::from(100),
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to have no units.",
|
||||
@ -37,14 +26,29 @@ pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
.into())
|
||||
}
|
||||
};
|
||||
Ok(Value::Dimension(num, Unit::Percent, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::Percent,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.round()), u, true)),
|
||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
||||
// todo: better error message, consider finities
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||
Err(("Infinity or NaN toInt", args.span()).into())
|
||||
}
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => Ok(Value::Dimension(SassNumber {
|
||||
num: (n.round()),
|
||||
unit: u,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -53,11 +57,22 @@ pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.ceil()), u, true)),
|
||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
||||
// todo: better error message, consider finities
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||
Err(("Infinity or NaN toInt", args.span()).into())
|
||||
}
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => Ok(Value::Dimension(SassNumber {
|
||||
num: (n.ceil()),
|
||||
unit: u,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -66,11 +81,22 @@ pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.floor()), u, true)),
|
||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
||||
// todo: better error message, consider finities
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||
Err(("Infinity or NaN toInt", args.span()).into())
|
||||
}
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => Ok(Value::Dimension(SassNumber {
|
||||
num: (n.floor()),
|
||||
unit: u,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -79,11 +105,18 @@ pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.abs()), u, true)),
|
||||
Value::Dimension(None, u, ..) => Ok(Value::Dimension(None, u, true)),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => Ok(Value::Dimension(SassNumber {
|
||||
num: (n.abs()),
|
||||
unit: u,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -92,10 +125,14 @@ pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let unit1 = match args.get_err(0, "number1")? {
|
||||
Value::Dimension(_, u, _) => u,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => u,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number1: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -105,7 +142,11 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
}
|
||||
};
|
||||
let unit2 = match args.get_err(1, "number2")? {
|
||||
Value::Dimension(_, u, _) => u,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => u,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number2: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -120,40 +161,28 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
|
||||
// TODO: write tests for this
|
||||
#[cfg(feature = "random")]
|
||||
pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let limit = match args.default_arg(0, "limit", Value::Null)? {
|
||||
Value::Dimension(Some(n), ..) => n,
|
||||
Value::Dimension(None, u, ..) => {
|
||||
return Err((format!("$limit: NaN{} is not an int.", u), args.span()).into())
|
||||
}
|
||||
Value::Null => {
|
||||
let limit = args.default_arg(0, "limit", Value::Null);
|
||||
|
||||
if matches!(limit, 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,
|
||||
));
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::from(rng.gen_range(0.0..1.0))),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$limit: {} is not a number.", v.inspect(args.span())?),
|
||||
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(Some(Number::one()), Unit::None, true));
|
||||
}
|
||||
|
||||
if limit.is_decimal() {
|
||||
return Err((
|
||||
format!("$limit: {} is not an int.", limit.inspect()),
|
||||
args.span(),
|
||||
)
|
||||
.into());
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::one()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
|
||||
if limit.is_zero() || limit.is_negative() {
|
||||
@ -164,134 +193,112 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
||||
.into());
|
||||
}
|
||||
|
||||
let limit = match limit.to_integer().to_u32() {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
return Err((
|
||||
format!(
|
||||
"max must be in range 0 < max \u{2264} 2^32, was {}",
|
||||
limit.inspect()
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
Ok(Value::Dimension(
|
||||
Some(Number::from(rng.gen_range(0..limit) + 1)),
|
||||
Unit::None,
|
||||
true,
|
||||
))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::from(rng.gen_range(0..limit_int) + 1)),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn min(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.min_args(1)?;
|
||||
let span = args.span();
|
||||
let mut nums = args
|
||||
.get_variadic()?
|
||||
.into_iter()
|
||||
.map(|val| match val.node {
|
||||
Value::Dimension(number, unit, _) => Ok((number, unit)),
|
||||
Value::Dimension(SassNumber {
|
||||
num: number,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => Ok((number, unit)),
|
||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||
})
|
||||
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
|
||||
.collect::<SassResult<Vec<(Number, Unit)>>>()?
|
||||
.into_iter();
|
||||
|
||||
let mut min = match nums.next() {
|
||||
Some((Some(n), u)) => (n, u),
|
||||
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
|
||||
Some((n, u)) => (n, u),
|
||||
None => unreachable!(),
|
||||
};
|
||||
|
||||
for (num, unit) in nums {
|
||||
let num = match num {
|
||||
Some(n) => n,
|
||||
None => continue,
|
||||
};
|
||||
let lhs = Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: unit.clone(),
|
||||
as_slash: None,
|
||||
});
|
||||
let rhs = Value::Dimension(SassNumber {
|
||||
num: (min.0),
|
||||
unit: min.1.clone(),
|
||||
as_slash: None,
|
||||
});
|
||||
|
||||
if ValueVisitor::new(parser, span)
|
||||
.less_than(
|
||||
HigherIntermediateValue::Literal(Value::Dimension(
|
||||
Some(num.clone()),
|
||||
unit.clone(),
|
||||
true,
|
||||
)),
|
||||
HigherIntermediateValue::Literal(Value::Dimension(
|
||||
Some(min.0.clone()),
|
||||
min.1.clone(),
|
||||
true,
|
||||
)),
|
||||
)?
|
||||
.is_true()
|
||||
{
|
||||
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_true() {
|
||||
min = (num, unit);
|
||||
}
|
||||
}
|
||||
Ok(Value::Dimension(Some(min.0), min.1, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (min.0),
|
||||
unit: min.1,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn max(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.min_args(1)?;
|
||||
let span = args.span();
|
||||
let mut nums = args
|
||||
.get_variadic()?
|
||||
.into_iter()
|
||||
.map(|val| match val.node {
|
||||
Value::Dimension(number, unit, _) => Ok((number, unit)),
|
||||
Value::Dimension(SassNumber {
|
||||
num: number,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => Ok((number, unit)),
|
||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||
})
|
||||
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
|
||||
.collect::<SassResult<Vec<(Number, Unit)>>>()?
|
||||
.into_iter();
|
||||
|
||||
let mut max = match nums.next() {
|
||||
Some((Some(n), u)) => (n, u),
|
||||
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
|
||||
Some((n, u)) => (n, u),
|
||||
None => unreachable!(),
|
||||
};
|
||||
|
||||
for (num, unit) in nums {
|
||||
let num = match num {
|
||||
Some(n) => n,
|
||||
None => continue,
|
||||
};
|
||||
let lhs = Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: unit.clone(),
|
||||
as_slash: None,
|
||||
});
|
||||
let rhs = Value::Dimension(SassNumber {
|
||||
num: (max.0),
|
||||
unit: max.1.clone(),
|
||||
as_slash: None,
|
||||
});
|
||||
|
||||
if ValueVisitor::new(parser, span)
|
||||
.greater_than(
|
||||
HigherIntermediateValue::Literal(Value::Dimension(
|
||||
Some(num.clone()),
|
||||
unit.clone(),
|
||||
true,
|
||||
)),
|
||||
HigherIntermediateValue::Literal(Value::Dimension(
|
||||
Some(max.0.clone()),
|
||||
max.1.clone(),
|
||||
true,
|
||||
)),
|
||||
)?
|
||||
.is_true()
|
||||
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)?.is_true()
|
||||
{
|
||||
max = (num, unit);
|
||||
}
|
||||
}
|
||||
Ok(Value::Dimension(Some(max.0), max.1, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: (max.0),
|
||||
unit: max.1,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn divide(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let number1 = args.get_err(0, "number1")?;
|
||||
let number2 = args.get_err(1, "number2")?;
|
||||
|
||||
ValueVisitor::new(parser, args.span()).eval(
|
||||
HigherIntermediateValue::BinaryOp(
|
||||
Box::new(HigherIntermediateValue::Literal(number1)),
|
||||
Op::Div,
|
||||
Box::new(HigherIntermediateValue::Literal(number2)),
|
||||
),
|
||||
true,
|
||||
)
|
||||
div(number1, number2, visitor.options, args.span())
|
||||
}
|
||||
|
||||
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||
|
@ -1,17 +1,28 @@
|
||||
use super::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use codemap::Spanned;
|
||||
// todo: this should be a constant of some sort. we shouldn't be allocating this
|
||||
// every time
|
||||
pub(crate) fn if_arguments() -> ArgumentDeclaration {
|
||||
ArgumentDeclaration {
|
||||
args: vec![
|
||||
Argument {
|
||||
name: Identifier::from("condition"),
|
||||
default: None,
|
||||
},
|
||||
Argument {
|
||||
name: Identifier::from("if-true"),
|
||||
default: None,
|
||||
},
|
||||
Argument {
|
||||
name: Identifier::from("if-false"),
|
||||
default: None,
|
||||
},
|
||||
],
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::{Identifier, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{SassFunction, Value},
|
||||
};
|
||||
|
||||
fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
if args.get_err(0, "condition")?.is_true() {
|
||||
Ok(args.get_err(1, "if-true")?)
|
||||
@ -20,7 +31,7 @@ fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "feature")? {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
@ -39,7 +50,7 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
||||
// The "Custom Properties Level 1" spec is supported. This means
|
||||
// that custom properties are parsed statically, with only
|
||||
// interpolation treated as SassScript.
|
||||
"custom-property" => Value::False,
|
||||
"custom-property" => Value::True,
|
||||
_ => Value::False,
|
||||
}),
|
||||
v => Err((
|
||||
@ -50,10 +61,14 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let unit = match args.get_err(0, "number")? {
|
||||
Value::Dimension(_, u, _) => u.to_string(),
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => u.to_string(),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -65,17 +80,21 @@ pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
||||
Ok(Value::String(unit, QuoteKind::Quoted))
|
||||
}
|
||||
|
||||
pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let value = args.get_err(0, "value")?;
|
||||
Ok(Value::String(value.kind().to_owned(), QuoteKind::None))
|
||||
}
|
||||
|
||||
pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(match args.get_err(0, "number")? {
|
||||
Value::Dimension(_, Unit::None, _) => Value::True,
|
||||
Value::Dimension(..) => Value::False,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: Unit::None,
|
||||
as_slash: _,
|
||||
}) => Value::True,
|
||||
Value::Dimension(SassNumber { .. }) => Value::False,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -86,7 +105,7 @@ pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(Value::String(
|
||||
args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
|
||||
@ -94,12 +113,13 @@ pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn variable_exists(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "name")? {
|
||||
Value::String(s, _) => Ok(Value::bool(
|
||||
parser.scopes.var_exists(s.into(), parser.global_scope),
|
||||
)),
|
||||
Value::String(s, _) => Ok(Value::bool(visitor.env.var_exists(s.into(), None)?)),
|
||||
v => Err((
|
||||
format!("$name: {} is not a string.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -108,7 +128,10 @@ pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn global_variable_exists(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let name: Identifier = match args.get_err(0, "name")? {
|
||||
@ -122,7 +145,7 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
|
||||
}
|
||||
};
|
||||
|
||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
||||
let module = match args.default_arg(1, "module", Value::Null) {
|
||||
Value::String(s, _) => Some(s),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
@ -135,16 +158,17 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
|
||||
};
|
||||
|
||||
Ok(Value::bool(if let Some(module_name) = module {
|
||||
parser
|
||||
.modules
|
||||
.get(module_name.into(), args.span())?
|
||||
(*(*visitor.env.modules)
|
||||
.borrow()
|
||||
.get(module_name.into(), args.span())?)
|
||||
.borrow()
|
||||
.var_exists(name)
|
||||
} else {
|
||||
parser.global_scope.var_exists(name)
|
||||
(*visitor.env.global_vars()).borrow().contains_key(&name)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let name: Identifier = match args.get_err(0, "name")? {
|
||||
Value::String(s, _) => s.into(),
|
||||
@ -157,7 +181,7 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
}
|
||||
};
|
||||
|
||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
||||
let module = match args.default_arg(1, "module", Value::Null) {
|
||||
Value::String(s, _) => Some(s),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
@ -170,16 +194,20 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
};
|
||||
|
||||
Ok(Value::bool(if let Some(module_name) = module {
|
||||
parser
|
||||
.modules
|
||||
.get(module_name.into(), args.span())?
|
||||
(*(*visitor.env.modules)
|
||||
.borrow()
|
||||
.get(module_name.into(), args.span())?)
|
||||
.borrow()
|
||||
.mixin_exists(name)
|
||||
} else {
|
||||
parser.scopes.mixin_exists(name, parser.global_scope)
|
||||
visitor.env.mixin_exists(name)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn function_exists(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let name: Identifier = match args.get_err(0, "name")? {
|
||||
@ -193,7 +221,7 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
||||
}
|
||||
};
|
||||
|
||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
||||
let module = match args.default_arg(1, "module", Value::Null) {
|
||||
Value::String(s, _) => Some(s),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
@ -206,16 +234,17 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
||||
};
|
||||
|
||||
Ok(Value::bool(if let Some(module_name) = module {
|
||||
parser
|
||||
.modules
|
||||
.get(module_name.into(), args.span())?
|
||||
(*(*visitor.env.modules)
|
||||
.borrow()
|
||||
.get(module_name.into(), args.span())?)
|
||||
.borrow()
|
||||
.fn_exists(name)
|
||||
} else {
|
||||
parser.scopes.fn_exists(name, parser.global_scope)
|
||||
visitor.env.fn_exists(name)
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let name: Identifier = match args.get_err(0, "name")? {
|
||||
Value::String(s, _) => s.into(),
|
||||
@ -227,8 +256,8 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let css = args.default_arg(1, "css", Value::False)?.is_true();
|
||||
let module = match args.default_arg(2, "module", Value::Null)? {
|
||||
let css = args.default_arg(1, "css", Value::False).is_true();
|
||||
let module = match args.default_arg(2, "module", Value::Null) {
|
||||
Value::String(s, ..) => Some(s),
|
||||
Value::Null => None,
|
||||
v => {
|
||||
@ -240,7 +269,7 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
}
|
||||
};
|
||||
|
||||
let func = match if let Some(module_name) = module {
|
||||
let func = if let Some(module_name) = module {
|
||||
if css {
|
||||
return Err((
|
||||
"$css and $module may not both be passed at once.",
|
||||
@ -249,68 +278,90 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
||||
.into());
|
||||
}
|
||||
|
||||
parser
|
||||
.modules
|
||||
.get(module_name.into(), args.span())?
|
||||
.get_fn(Spanned {
|
||||
node: name,
|
||||
visitor.env.get_fn(
|
||||
name,
|
||||
Some(Spanned {
|
||||
node: module_name.into(),
|
||||
span: args.span(),
|
||||
})?
|
||||
}),
|
||||
)?
|
||||
} else {
|
||||
parser.scopes.get_fn(name, parser.global_scope)
|
||||
} {
|
||||
Some(f) => f,
|
||||
None => match GLOBAL_FUNCTIONS.get(name.as_str()) {
|
||||
Some(f) => SassFunction::Builtin(f.clone(), name),
|
||||
None => return Err((format!("Function not found: {}", name), args.span()).into()),
|
||||
},
|
||||
match visitor.env.get_fn(name, None)? {
|
||||
Some(f) => Some(f),
|
||||
None => GLOBAL_FUNCTIONS
|
||||
.get(name.as_str())
|
||||
.map(|f| SassFunction::Builtin(f.clone(), name)),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::FunctionRef(func))
|
||||
match func {
|
||||
Some(func) => Ok(Value::FunctionRef(func)),
|
||||
None => Err((format!("Function not found: {}", name), args.span()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let span = args.span();
|
||||
let func = match args.get_err(0, "function")? {
|
||||
Value::FunctionRef(f) => f,
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"$function: {} is not a function reference.",
|
||||
v.inspect(args.span())?
|
||||
v.inspect(span)?
|
||||
),
|
||||
args.span(),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
func.call(args.decrement(), None, parser)
|
||||
|
||||
args.remove_positional(0).unwrap();
|
||||
|
||||
visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn content_exists(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(0)?;
|
||||
if !parser.flags.in_mixin() {
|
||||
if !visitor.flags.in_mixin() {
|
||||
return Err((
|
||||
"content-exists() may only be called within a mixin.",
|
||||
parser.span_before,
|
||||
args.span(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(Value::bool(
|
||||
parser.content.last().map_or(false, |c| c.content.is_some()),
|
||||
))
|
||||
Ok(Value::bool(visitor.env.content.is_some()))
|
||||
}
|
||||
|
||||
#[allow(unused_variables, clippy::needless_pass_by_value)]
|
||||
pub(crate) fn keywords(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
Err((
|
||||
"Builtin function `keywords` is not yet implemented",
|
||||
args.span(),
|
||||
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) {
|
||||
f.insert("if", Builtin::new(if_));
|
||||
|
@ -2,13 +2,13 @@
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{BTreeSet, HashMap},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value};
|
||||
use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
@ -21,16 +21,19 @@ pub mod meta;
|
||||
pub mod selector;
|
||||
pub mod string;
|
||||
|
||||
// todo: maybe Identifier instead of str?
|
||||
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
|
||||
|
||||
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
// TODO: impl Fn
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Builtin(pub fn(CallArgs, &mut Parser) -> SassResult<Value>, usize);
|
||||
pub(crate) struct Builtin(
|
||||
pub fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
|
||||
usize,
|
||||
);
|
||||
|
||||
impl Builtin {
|
||||
pub fn new(body: fn(CallArgs, &mut Parser) -> SassResult<Value>) -> Builtin {
|
||||
pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>) -> Builtin {
|
||||
let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
Self(body, count)
|
||||
}
|
||||
@ -55,3 +58,24 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<GlobalFunctionMap> = Lazy::new(|| {
|
||||
string::declare(&mut m);
|
||||
m
|
||||
});
|
||||
|
||||
pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy<BTreeSet<&str>> = Lazy::new(|| {
|
||||
GLOBAL_FUNCTIONS
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|&name| {
|
||||
!matches!(
|
||||
name,
|
||||
"rgb"
|
||||
| "rgba"
|
||||
| "hsl"
|
||||
| "hsla"
|
||||
| "grayscale"
|
||||
| "invert"
|
||||
| "alpha"
|
||||
| "opacity"
|
||||
| "saturate"
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
@ -1,32 +1,36 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::{Brackets, ListSeparator, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
selector::{ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList},
|
||||
value::Value,
|
||||
use crate::selector::{
|
||||
ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList,
|
||||
};
|
||||
use crate::serializer::serialize_selector_list;
|
||||
|
||||
pub(crate) fn is_superselector(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn is_superselector(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let parent_selector = args
|
||||
.get_err(0, "super")?
|
||||
.to_selector(parser, "super", false)?;
|
||||
let child_selector = args.get_err(1, "sub")?.to_selector(parser, "sub", false)?;
|
||||
let parent_selector =
|
||||
args.get_err(0, "super")?
|
||||
.to_selector(visitor, "super", false, args.span())?;
|
||||
let child_selector = args
|
||||
.get_err(1, "sub")?
|
||||
.to_selector(visitor, "sub", false, args.span())?;
|
||||
|
||||
Ok(Value::bool(
|
||||
parent_selector.is_super_selector(&child_selector),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn simple_selectors(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
// todo: Value::to_compound_selector
|
||||
let selector = args
|
||||
.get_err(0, "selector")?
|
||||
.to_selector(parser, "selector", false)?;
|
||||
let selector =
|
||||
args.get_err(0, "selector")?
|
||||
.to_selector(visitor, "selector", false, args.span())?;
|
||||
|
||||
if selector.0.components.len() != 1 {
|
||||
return Err(("$selector: expected selector.", args.span()).into());
|
||||
@ -51,16 +55,16 @@ pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassR
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn selector_parse(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
Ok(args
|
||||
.get_err(0, "selector")?
|
||||
.to_selector(parser, "selector", false)
|
||||
.to_selector(visitor, "selector", false, args.span())
|
||||
.map_err(|_| ("$selector: expected selector.", args.span()))?
|
||||
.into_value())
|
||||
}
|
||||
|
||||
pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let span = args.span();
|
||||
let selectors = args.get_variadic()?;
|
||||
if selectors.is_empty() {
|
||||
@ -69,7 +73,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
|
||||
Ok(selectors
|
||||
.into_iter()
|
||||
.map(|sel| sel.node.to_selector(parser, "selectors", true))
|
||||
.map(|sel| sel.node.to_selector(visitor, "selectors", true, span))
|
||||
.collect::<SassResult<Vec<Selector>>>()?
|
||||
.into_iter()
|
||||
.try_fold(
|
||||
@ -81,7 +85,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
.into_value())
|
||||
}
|
||||
|
||||
pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
let span = args.span();
|
||||
let selectors = args.get_variadic()?;
|
||||
if selectors.is_empty() {
|
||||
@ -90,7 +94,7 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
|
||||
let mut parsed_selectors = selectors
|
||||
.into_iter()
|
||||
.map(|s| s.node.to_selector(parser, "selectors", false))
|
||||
.map(|s| s.node.to_selector(visitor, "selectors", false, span))
|
||||
.collect::<SassResult<Vec<Selector>>>()?;
|
||||
|
||||
let first = parsed_selectors.remove(0);
|
||||
@ -109,7 +113,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
Some(v) => ComplexSelectorComponent::Compound(v),
|
||||
None => {
|
||||
return Err((
|
||||
format!("Can't append {} to {}.", complex, parent),
|
||||
format!(
|
||||
"Can't append {} to {}.",
|
||||
complex,
|
||||
serialize_selector_list(
|
||||
&parent.0,
|
||||
visitor.options,
|
||||
span
|
||||
)
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
@ -118,7 +130,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
components.extend(complex.components.into_iter().skip(1));
|
||||
Ok(ComplexSelector::new(components, false))
|
||||
} else {
|
||||
Err((format!("Can't append {} to {}.", complex, parent), span).into())
|
||||
Err((
|
||||
format!(
|
||||
"Can't append {} to {}.",
|
||||
complex,
|
||||
serialize_selector_list(&parent.0, visitor.options, span)
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
.collect::<SassResult<Vec<ComplexSelector>>>()?,
|
||||
@ -129,40 +149,46 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
||||
.into_value())
|
||||
}
|
||||
|
||||
pub(crate) fn selector_extend(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_extend(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let selector = args
|
||||
.get_err(0, "selector")?
|
||||
.to_selector(parser, "selector", false)?;
|
||||
let target = args
|
||||
.get_err(1, "extendee")?
|
||||
.to_selector(parser, "extendee", false)?;
|
||||
let source = args
|
||||
.get_err(2, "extender")?
|
||||
.to_selector(parser, "extender", false)?;
|
||||
let selector =
|
||||
args.get_err(0, "selector")?
|
||||
.to_selector(visitor, "selector", false, args.span())?;
|
||||
let target =
|
||||
args.get_err(1, "extendee")?
|
||||
.to_selector(visitor, "extendee", false, args.span())?;
|
||||
let source =
|
||||
args.get_err(2, "extender")?
|
||||
.to_selector(visitor, "extender", false, args.span())?;
|
||||
|
||||
Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||
Ok(ExtensionStore::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||
}
|
||||
|
||||
pub(crate) fn selector_replace(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_replace(
|
||||
mut args: ArgumentResult,
|
||||
visitor: &mut Visitor,
|
||||
) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let selector = args
|
||||
.get_err(0, "selector")?
|
||||
.to_selector(parser, "selector", true)?;
|
||||
let target = args
|
||||
.get_err(1, "original")?
|
||||
.to_selector(parser, "original", true)?;
|
||||
let source = args
|
||||
.get_err(2, "replacement")?
|
||||
.to_selector(parser, "replacement", true)?;
|
||||
Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||
let selector =
|
||||
args.get_err(0, "selector")?
|
||||
.to_selector(visitor, "selector", true, args.span())?;
|
||||
let target =
|
||||
args.get_err(1, "original")?
|
||||
.to_selector(visitor, "original", true, args.span())?;
|
||||
let source =
|
||||
args.get_err(2, "replacement")?
|
||||
.to_selector(visitor, "replacement", true, args.span())?;
|
||||
Ok(ExtensionStore::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||
}
|
||||
|
||||
pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let selector1 = args
|
||||
.get_err(0, "selector1")?
|
||||
.to_selector(parser, "selector1", true)?;
|
||||
let selector1 =
|
||||
args.get_err(0, "selector1")?
|
||||
.to_selector(visitor, "selector1", true, args.span())?;
|
||||
|
||||
if selector1.contains_parent_selector() {
|
||||
return Err((
|
||||
@ -172,9 +198,9 @@ pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
||||
.into());
|
||||
}
|
||||
|
||||
let selector2 = args
|
||||
.get_err(1, "selector2")?
|
||||
.to_selector(parser, "selector2", true)?;
|
||||
let selector2 =
|
||||
args.get_err(1, "selector2")?
|
||||
.to_selector(visitor, "selector2", true, args.span())?;
|
||||
|
||||
if selector2.contains_parent_selector() {
|
||||
return Err((
|
||||
|
@ -1,21 +1,6 @@
|
||||
use super::{Builtin, GlobalFunctionMap};
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use num_bigint::BigInt;
|
||||
use num_traits::{Signed, ToPrimitive, Zero};
|
||||
|
||||
#[cfg(feature = "random")]
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
common::QuoteKind,
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
unit::Unit,
|
||||
value::{Number, Value},
|
||||
};
|
||||
|
||||
pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "string")? {
|
||||
Value::String(mut i, q) => {
|
||||
@ -30,7 +15,7 @@ pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "string")? {
|
||||
Value::String(mut i, q) => {
|
||||
@ -45,14 +30,14 @@ pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "string")? {
|
||||
Value::String(i, _) => Ok(Value::Dimension(
|
||||
Some(Number::from(i.chars().count())),
|
||||
Unit::None,
|
||||
true,
|
||||
)),
|
||||
Value::String(i, _) => Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::from(i.chars().count())),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
})),
|
||||
v => Err((
|
||||
format!("$string: {} is not a string.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
@ -61,7 +46,7 @@ pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "string")? {
|
||||
Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
|
||||
@ -73,7 +58,7 @@ pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
match args.get_err(0, "string")? {
|
||||
i @ Value::String(..) => Ok(i.unquote()),
|
||||
@ -85,8 +70,11 @@ pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
|
||||
let span = args.span();
|
||||
|
||||
let (string, quotes) = match args.get_err(0, "string")? {
|
||||
Value::String(s, q) => (s, q),
|
||||
v => {
|
||||
@ -97,79 +85,46 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let str_len = string.chars().count();
|
||||
let start = match args.get_err(1, "start-at")? {
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
||||
return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
|
||||
n.to_integer().to_usize().unwrap_or(str_len + 1)
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize,
|
||||
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize,
|
||||
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
|
||||
.to_usize()
|
||||
.unwrap(),
|
||||
Value::Dimension(None, Unit::None, ..) => {
|
||||
return Err(("NaN is not an int.", args.span()).into())
|
||||
}
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$start: Expected {} to have no units.",
|
||||
v.inspect(args.span())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$start-at: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let mut end = match args.default_arg(2, "end-at", Value::Null)? {
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
||||
return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
|
||||
n.to_integer().to_usize().unwrap_or(str_len + 1)
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize,
|
||||
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize,
|
||||
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
|
||||
.to_usize()
|
||||
.unwrap_or(str_len + 1),
|
||||
Value::Dimension(None, Unit::None, ..) => {
|
||||
return Err(("NaN is not an int.", args.span()).into())
|
||||
}
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$end: Expected {} to have no units.",
|
||||
v.inspect(args.span())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Null => str_len,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$end-at: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
let start = args
|
||||
.get_err(1, "start-at")?
|
||||
.assert_number_with_name("start-at", span)?;
|
||||
start.assert_no_units("start-at", span)?;
|
||||
|
||||
let start = start.num().assert_int(span)?;
|
||||
|
||||
let start = if start == 0 {
|
||||
1
|
||||
} else if start > 0 {
|
||||
(start as usize).min(str_len + 1)
|
||||
} else {
|
||||
(start + str_len as i32 + 1).max(1) as usize
|
||||
};
|
||||
|
||||
if end > str_len {
|
||||
end = str_len;
|
||||
let end = args
|
||||
.default_arg(
|
||||
2,
|
||||
"end-at",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number(-1.0),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
)
|
||||
.assert_number_with_name("end-at", span)?;
|
||||
|
||||
end.assert_no_units("end-at", span)?;
|
||||
|
||||
let mut end = end.num().assert_int(span)?;
|
||||
|
||||
if end < 0 {
|
||||
end += str_len as i32 + 1;
|
||||
}
|
||||
|
||||
let end = (end.max(0) as usize).min(str_len + 1);
|
||||
|
||||
if start > end || start > str_len {
|
||||
Ok(Value::String(String::new(), quotes))
|
||||
} else {
|
||||
@ -184,7 +139,7 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let s1 = match args.get_err(0, "string")? {
|
||||
Value::String(i, _) => i,
|
||||
@ -209,19 +164,25 @@ pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
||||
};
|
||||
|
||||
Ok(match s1.find(&substr) {
|
||||
Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true),
|
||||
Some(v) => Value::Dimension(SassNumber {
|
||||
num: (Number::from(v + 1)),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
None => Value::Null,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let span = args.span();
|
||||
|
||||
let (s1, quotes) = match args.get_err(0, "string")? {
|
||||
Value::String(i, q) => (i, q),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$string: {} is not a string.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
format!("$string: {} is not a string.", v.inspect(span)?),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
@ -231,43 +192,18 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
Value::String(i, _) => i,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$insert: {} is not a string.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
format!("$insert: {} is not a string.", v.inspect(span)?),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let index = match args.get_err(2, "index")? {
|
||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
||||
return Err((
|
||||
format!("$index: {} is not an int.", n.inspect()),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
||||
Value::Dimension(None, Unit::None, ..) => {
|
||||
return Err(("$index: NaN is not an int.", args.span()).into())
|
||||
}
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$index: Expected {} to have no units.",
|
||||
v.inspect(args.span())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v => {
|
||||
return Err((
|
||||
format!("$index: {} is not a number.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let index = args
|
||||
.get_err(2, "index")?
|
||||
.assert_number_with_name("index", span)?;
|
||||
index.assert_no_units("index", span)?;
|
||||
let index_int = index.num().assert_int_with_name("index", span)?;
|
||||
|
||||
if s1.is_empty() {
|
||||
return Ok(Value::String(substr, quotes));
|
||||
@ -291,26 +227,13 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
.collect::<String>()
|
||||
};
|
||||
|
||||
let string = if index.is_positive() {
|
||||
insert(
|
||||
index
|
||||
.to_integer()
|
||||
.to_usize()
|
||||
.unwrap_or(len + 1)
|
||||
.min(len + 1)
|
||||
- 1,
|
||||
s1,
|
||||
&substr,
|
||||
)
|
||||
} else if index.is_zero() {
|
||||
let string = if index_int > 0 {
|
||||
insert((index_int as usize - 1).min(len), s1, &substr)
|
||||
} else if index_int == 0 {
|
||||
insert(0, s1, &substr)
|
||||
} else {
|
||||
let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1);
|
||||
if idx > len {
|
||||
insert(0, s1, &substr)
|
||||
} else {
|
||||
insert(len - idx + 1, s1, &substr)
|
||||
}
|
||||
let idx = (len as i32 + index_int + 1).max(0) as usize;
|
||||
insert(idx, s1, &substr)
|
||||
};
|
||||
|
||||
Ok(Value::String(string, quotes))
|
||||
@ -318,15 +241,15 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
||||
|
||||
#[cfg(feature = "random")]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn unique_id(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(0)?;
|
||||
let mut rng = thread_rng();
|
||||
let string = std::iter::repeat(())
|
||||
let string: String = std::iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(7)
|
||||
.take(12)
|
||||
.collect();
|
||||
Ok(Value::String(string, QuoteKind::None))
|
||||
Ok(Value::String(format!("id-{}", string), QuoteKind::None))
|
||||
}
|
||||
|
||||
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||
|
@ -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},
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,32 @@
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use crate::builtin::{
|
||||
list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip},
|
||||
modules::Module,
|
||||
};
|
||||
|
||||
// todo: write tests for this
|
||||
fn slash(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.min_args(1)?;
|
||||
|
||||
let span = args.span();
|
||||
|
||||
let list = if args.len() == 1 {
|
||||
args.get_err(0, "elements")?.as_list()
|
||||
} else {
|
||||
args.get_variadic()?
|
||||
.into_iter()
|
||||
.map(|arg| arg.node)
|
||||
.collect()
|
||||
};
|
||||
|
||||
if list.len() < 2 {
|
||||
return Err(("At least two elements are required.", span).into());
|
||||
}
|
||||
|
||||
Ok(Value::List(list, ListSeparator::Slash, Brackets::None))
|
||||
}
|
||||
|
||||
pub(crate) fn declare(f: &mut Module) {
|
||||
f.insert_builtin("append", append);
|
||||
f.insert_builtin("index", index);
|
||||
@ -13,4 +37,5 @@ pub(crate) fn declare(f: &mut Module) {
|
||||
f.insert_builtin("nth", nth);
|
||||
f.insert_builtin("set-nth", set_nth);
|
||||
f.insert_builtin("zip", zip);
|
||||
f.insert_builtin("slash", slash);
|
||||
}
|
||||
|
@ -1,30 +1,36 @@
|
||||
use std::cmp::Ordering;
|
||||
use crate::builtin::builtin_imports::*;
|
||||
|
||||
use num_traits::{One, Signed, Zero};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
builtin::{
|
||||
use crate::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},
|
||||
};
|
||||
|
||||
#[cfg(feature = "random")]
|
||||
use crate::builtin::math::random;
|
||||
use crate::value::{conversion_factor, SassNumber};
|
||||
|
||||
fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn coerce_to_rad(num: f64, unit: Unit) -> f64 {
|
||||
debug_assert!(matches!(
|
||||
unit,
|
||||
Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn
|
||||
));
|
||||
|
||||
if unit == Unit::None {
|
||||
return num;
|
||||
}
|
||||
|
||||
let factor = conversion_factor(&unit, &Unit::Rad).unwrap();
|
||||
|
||||
num * factor
|
||||
}
|
||||
|
||||
fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(3)?;
|
||||
let span = args.span();
|
||||
|
||||
let min = match args.get_err(0, "min")? {
|
||||
v @ Value::Dimension(..) => v,
|
||||
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$min: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -35,7 +41,7 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
|
||||
let number = match args.get_err(1, "number")? {
|
||||
v @ Value::Dimension(..) => v,
|
||||
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(span)?),
|
||||
@ -46,23 +52,35 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
|
||||
let max = match args.get_err(2, "max")? {
|
||||
v @ Value::Dimension(..) => v,
|
||||
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
|
||||
};
|
||||
|
||||
// ensure that `min` and `max` are compatible
|
||||
min.cmp(&max, span, Op::LessThan)?;
|
||||
min.cmp(&max, span, BinaryOp::LessThan)?;
|
||||
|
||||
let min_unit = match min {
|
||||
Value::Dimension(_, ref u, _) => u,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: ref u,
|
||||
as_slash: _,
|
||||
}) => u,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let number_unit = match number {
|
||||
Value::Dimension(_, ref u, _) => u,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: ref u,
|
||||
as_slash: _,
|
||||
}) => u,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let max_unit = match max {
|
||||
Value::Dimension(_, ref u, _) => u,
|
||||
Value::Dimension(SassNumber {
|
||||
num: _,
|
||||
unit: ref u,
|
||||
as_slash: _,
|
||||
}) => u,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
@ -86,45 +104,43 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
), span).into());
|
||||
}
|
||||
|
||||
match min.cmp(&number, span, Op::LessThan)? {
|
||||
Ordering::Greater => return Ok(min),
|
||||
Ordering::Equal => return Ok(number),
|
||||
Ordering::Less => {}
|
||||
match min.cmp(&number, span, BinaryOp::LessThan)? {
|
||||
Some(Ordering::Greater) => return Ok(min),
|
||||
Some(Ordering::Equal) => return Ok(number),
|
||||
Some(Ordering::Less) | None => {}
|
||||
}
|
||||
|
||||
match max.cmp(&number, span, Op::GreaterThan)? {
|
||||
Ordering::Less => return Ok(max),
|
||||
Ordering::Equal => return Ok(number),
|
||||
Ordering::Greater => {}
|
||||
match max.cmp(&number, span, BinaryOp::GreaterThan)? {
|
||||
Some(Ordering::Less) => return Ok(max),
|
||||
Some(Ordering::Equal) => return Ok(number),
|
||||
Some(Ordering::Greater) | None => {}
|
||||
}
|
||||
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.min_args(1)?;
|
||||
|
||||
let span = args.span();
|
||||
|
||||
let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> {
|
||||
match v.node {
|
||||
Value::Dimension(n, u, ..) => Ok((n, u)),
|
||||
Value::Dimension(SassNumber { num, unit, .. }) => Ok((num, unit)),
|
||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||
}
|
||||
});
|
||||
|
||||
let first: (Number, Unit) = match numbers.next().unwrap()? {
|
||||
(Some(n), u) => (n.clone() * n, u),
|
||||
(None, u) => return Ok(Value::Dimension(None, u, true)),
|
||||
};
|
||||
let (n, u) = numbers.next().unwrap()?;
|
||||
let first: (Number, Unit) = (n * n, u);
|
||||
|
||||
let rest = numbers
|
||||
.enumerate()
|
||||
.map(|(idx, val)| -> SassResult<Option<Number>> {
|
||||
.map(|(idx, val)| -> SassResult<Number> {
|
||||
let (number, unit) = val?;
|
||||
if first.1 == Unit::None {
|
||||
if unit == Unit::None {
|
||||
Ok(number.map(|n| n.clone() * n))
|
||||
Ok(number * number)
|
||||
} else {
|
||||
Err((
|
||||
format!(
|
||||
@ -149,9 +165,8 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
} else if first.1.comparable(&unit) {
|
||||
Ok(number
|
||||
.map(|n| n.convert(&unit, &first.1))
|
||||
.map(|n| n.clone() * n))
|
||||
let n = number.convert(&unit, &first.1);
|
||||
Ok(n * n)
|
||||
} else {
|
||||
Err((
|
||||
format!("Incompatible units {} and {}.", first.1, unit),
|
||||
@ -160,24 +175,28 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
.into())
|
||||
}
|
||||
})
|
||||
.collect::<SassResult<Option<Vec<Number>>>>()?;
|
||||
|
||||
let rest = match rest {
|
||||
Some(v) => v,
|
||||
None => return Ok(Value::Dimension(None, first.1, true)),
|
||||
};
|
||||
.collect::<SassResult<Vec<Number>>>()?;
|
||||
|
||||
let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b);
|
||||
|
||||
Ok(Value::Dimension(sum.sqrt(), first.1, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: sum.sqrt(),
|
||||
unit: first.1,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let number = match args.get_err(0, "number")? {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
// Value::Dimension { num: n, .. } if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => num,
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be unitless.",
|
||||
@ -187,7 +206,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v @ Value::Dimension(None, ..) => return Ok(v),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -197,10 +215,14 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
|
||||
let base = match args.default_arg(1, "base", Value::Null)? {
|
||||
let base = match args.default_arg(1, "base", Value::Null) {
|
||||
Value::Null => None,
|
||||
Value::Dimension(Some(n), Unit::None, ..) => Some(n),
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => Some(num),
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be unitless.",
|
||||
@ -210,7 +232,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
v @ Value::Dimension(None, ..) => return Ok(v),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -220,31 +241,36 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::Dimension(
|
||||
if let Some(base) = base {
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: if let Some(base) = base {
|
||||
if base.is_zero() {
|
||||
Some(Number::zero())
|
||||
Number::zero()
|
||||
} else {
|
||||
number.log(base)
|
||||
}
|
||||
} else if number.is_negative() {
|
||||
None
|
||||
// todo: test with negative 0
|
||||
} else if number.is_negative() && !number.is_zero() {
|
||||
Number(f64::NAN)
|
||||
} else if number.is_zero() {
|
||||
todo!()
|
||||
Number(f64::NEG_INFINITY)
|
||||
} else {
|
||||
number.ln()
|
||||
},
|
||||
Unit::None,
|
||||
true,
|
||||
))
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let base = match args.get_err(0, "base")? {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => num,
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$base: Expected {} to have no units.",
|
||||
@ -254,7 +280,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -265,8 +290,12 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
|
||||
let exponent = match args.get_err(1, "exponent")? {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => num,
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$exponent: Expected {} to have no units.",
|
||||
@ -276,7 +305,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$exponent: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -286,16 +314,28 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::Dimension(base.pow(exponent), Unit::None, true))
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: base.pow(exponent),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let number = args.get_err(0, "number")?;
|
||||
|
||||
Ok(match number {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(n.sqrt(), Unit::None, true),
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => Value::Dimension(SassNumber {
|
||||
num: num.sqrt(),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to have no units.",
|
||||
@ -305,7 +345,6 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -317,30 +356,32 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
}
|
||||
|
||||
macro_rules! trig_fn {
|
||||
($name:ident, $name_deg:ident) => {
|
||||
fn $name(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
($name:ident) => {
|
||||
fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let number = args.get_err(0, "number")?;
|
||||
|
||||
Ok(match number {
|
||||
Value::Dimension(Some(n), Unit::None, ..)
|
||||
| Value::Dimension(Some(n), Unit::Rad, ..) => {
|
||||
Value::Dimension(n.$name(), Unit::None, true)
|
||||
}
|
||||
Value::Dimension(Some(n), Unit::Deg, ..) => {
|
||||
Value::Dimension(n.$name_deg(), Unit::None, true)
|
||||
}
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn),
|
||||
..
|
||||
}) => Value::Dimension(SassNumber {
|
||||
num: Number(coerce_to_rad(num.0, unit).$name()),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
v @ Value::Dimension(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be an angle.",
|
||||
"$number: Expected {} to have an angle unit (deg, grad, rad, turn).",
|
||||
v.inspect(args.span())?
|
||||
),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -353,27 +394,31 @@ macro_rules! trig_fn {
|
||||
};
|
||||
}
|
||||
|
||||
trig_fn!(cos, cos_deg);
|
||||
trig_fn!(sin, sin_deg);
|
||||
trig_fn!(tan, tan_deg);
|
||||
trig_fn!(cos);
|
||||
trig_fn!(sin);
|
||||
trig_fn!(tan);
|
||||
|
||||
fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let number = args.get_err(0, "number")?;
|
||||
|
||||
Ok(match number {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(
|
||||
if n > Number::from(1) || n < Number::from(-1) {
|
||||
None
|
||||
} else if n.is_one() {
|
||||
Some(Number::zero())
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => Value::Dimension(SassNumber {
|
||||
num: if num > Number::from(1) || num < Number::from(-1) {
|
||||
Number(f64::NAN)
|
||||
} else if num.is_one() {
|
||||
Number::zero()
|
||||
} else {
|
||||
n.acos()
|
||||
num.acos()
|
||||
},
|
||||
Unit::Deg,
|
||||
true,
|
||||
),
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
}),
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be unitless.",
|
||||
@ -383,7 +428,6 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -394,21 +438,37 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let number = args.get_err(0, "number")?;
|
||||
|
||||
Ok(match number {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => {
|
||||
if n > Number::from(1) || n < Number::from(-1) {
|
||||
return Ok(Value::Dimension(None, Unit::Deg, true));
|
||||
} else if n.is_zero() {
|
||||
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true));
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => {
|
||||
if num > Number::from(1) || num < Number::from(-1) {
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: Number(f64::NAN),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
}));
|
||||
} else if num.is_zero() {
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: Number::zero(),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Value::Dimension(n.asin(), Unit::Deg, true)
|
||||
Value::Dimension(SassNumber {
|
||||
num: num.asin(),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
})
|
||||
}
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be unitless.",
|
||||
@ -418,7 +478,6 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -429,19 +488,31 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
let number = args.get_err(0, "number")?;
|
||||
|
||||
Ok(match number {
|
||||
Value::Dimension(Some(n), Unit::None, ..) => {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: Unit::None,
|
||||
..
|
||||
}) => {
|
||||
if n.is_zero() {
|
||||
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true));
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: (Number::zero()),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Value::Dimension(n.atan(), Unit::Deg, true)
|
||||
Value::Dimension(SassNumber {
|
||||
num: n.atan(),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
})
|
||||
}
|
||||
v @ Value::Dimension(Some(..), ..) => {
|
||||
v @ Value::Dimension(SassNumber { .. }) => {
|
||||
return Err((
|
||||
format!(
|
||||
"$number: Expected {} to be unitless.",
|
||||
@ -451,7 +522,6 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -462,10 +532,12 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(2)?;
|
||||
let (y_num, y_unit) = match args.get_err(0, "y")? {
|
||||
Value::Dimension(n, u, ..) => (n, u),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) => (n, u),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$y: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -476,7 +548,9 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
|
||||
let (x_num, x_unit) = match args.get_err(1, "x")? {
|
||||
Value::Dimension(n, u, ..) => (n, u),
|
||||
Value::Dimension(SassNumber {
|
||||
num: n, unit: u, ..
|
||||
}) => (n, u),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$x: {} is not a number.", v.inspect(args.span())?),
|
||||
@ -487,17 +561,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
};
|
||||
|
||||
let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None {
|
||||
let x = match x_num {
|
||||
Some(n) => n,
|
||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
||||
};
|
||||
|
||||
let y = match y_num {
|
||||
Some(n) => n,
|
||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
||||
};
|
||||
|
||||
(x, y)
|
||||
(x_num, y_num)
|
||||
} else if y_unit == Unit::None {
|
||||
return Err((
|
||||
format!(
|
||||
@ -519,17 +583,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
)
|
||||
.into());
|
||||
} else if x_unit.comparable(&y_unit) {
|
||||
let x = match x_num {
|
||||
Some(n) => n,
|
||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
||||
};
|
||||
|
||||
let y = match y_num {
|
||||
Some(n) => n,
|
||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
||||
};
|
||||
|
||||
(x, y.convert(&y_unit, &x_unit))
|
||||
(x_num, y_num.convert(&y_unit, &x_unit))
|
||||
} else {
|
||||
return Err((
|
||||
format!("Incompatible units {} and {}.", y_unit, x_unit),
|
||||
@ -538,51 +592,11 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
||||
.into());
|
||||
};
|
||||
|
||||
Ok(
|
||||
match (
|
||||
NumberState::from_number(&x_num),
|
||||
NumberState::from_number(&y_num),
|
||||
) {
|
||||
(NumberState::Zero, NumberState::FiniteNegative) => {
|
||||
Value::Dimension(Some(Number::from(-90)), Unit::Deg, true)
|
||||
}
|
||||
(NumberState::Zero, NumberState::Zero) | (NumberState::Finite, NumberState::Zero) => {
|
||||
Value::Dimension(Some(Number::zero()), Unit::Deg, true)
|
||||
}
|
||||
(NumberState::Zero, NumberState::Finite) => {
|
||||
Value::Dimension(Some(Number::from(90)), Unit::Deg, true)
|
||||
}
|
||||
(NumberState::Finite, NumberState::Finite)
|
||||
| (NumberState::FiniteNegative, NumberState::Finite)
|
||||
| (NumberState::Finite, NumberState::FiniteNegative)
|
||||
| (NumberState::FiniteNegative, NumberState::FiniteNegative) => Value::Dimension(
|
||||
y_num
|
||||
.atan2(x_num)
|
||||
.map(|n| (n * Number::from(180)) / Number::pi()),
|
||||
Unit::Deg,
|
||||
true,
|
||||
),
|
||||
(NumberState::FiniteNegative, NumberState::Zero) => {
|
||||
Value::Dimension(Some(Number::from(180)), Unit::Deg, true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum NumberState {
|
||||
Zero,
|
||||
Finite,
|
||||
FiniteNegative,
|
||||
}
|
||||
|
||||
impl NumberState {
|
||||
fn from_number(num: &Number) -> Self {
|
||||
match (num.is_zero(), num.is_positive()) {
|
||||
(true, _) => NumberState::Zero,
|
||||
(false, true) => NumberState::Finite,
|
||||
(false, false) => NumberState::FiniteNegative,
|
||||
}
|
||||
}
|
||||
Ok(Value::Dimension(SassNumber {
|
||||
num: Number(y_num.0.atan2(x_num.0).to_degrees()),
|
||||
unit: Unit::Deg,
|
||||
as_slash: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn declare(f: &mut Module) {
|
||||
@ -614,10 +628,58 @@ pub(crate) fn declare(f: &mut Module) {
|
||||
|
||||
f.insert_builtin_var(
|
||||
"e",
|
||||
Value::Dimension(Some(Number::from(std::f64::consts::E)), Unit::None, true),
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(std::f64::consts::E),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"pi",
|
||||
Value::Dimension(Some(Number::from(std::f64::consts::PI)), Unit::None, true),
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(std::f64::consts::PI),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"epsilon",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(std::f64::EPSILON),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"max-safe-integer",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(9007199254740991.0),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"min-safe-integer",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(-9007199254740991.0),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"max-number",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(f64::MAX),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
f.insert_builtin_var(
|
||||
"min-number",
|
||||
Value::Dimension(SassNumber {
|
||||
num: Number::from(f64::MIN_POSITIVE),
|
||||
unit: Unit::None,
|
||||
as_slash: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
use codemap::Spanned;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
builtin::{
|
||||
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,
|
||||
},
|
||||
modules::{Module, ModuleConfig},
|
||||
},
|
||||
error::SassResult,
|
||||
parse::{Parser, Stmt},
|
||||
value::Value,
|
||||
modules::Module,
|
||||
};
|
||||
use crate::serializer::serialize_calculation_arg;
|
||||
|
||||
fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
||||
fn load_css(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<()> {
|
||||
args.max_args(2)?;
|
||||
|
||||
let span = args.span();
|
||||
@ -30,19 +30,21 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
||||
}
|
||||
};
|
||||
|
||||
let with = match args.default_arg(1, "with", Value::Null)? {
|
||||
let with = match args.default_arg(1, "with", Value::Null) {
|
||||
Value::Map(map) => Some(map),
|
||||
Value::Null => None,
|
||||
v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()),
|
||||
};
|
||||
|
||||
// todo: tests for `with`
|
||||
if let Some(with) = with {
|
||||
let mut config = ModuleConfig::default();
|
||||
let mut configuration = Configuration::empty();
|
||||
|
||||
if let Some(with) = with {
|
||||
visitor.emit_warning("`grass` does not currently support the $with parameter of load-css. This file will be imported the same way it would using `@import`.", args.span());
|
||||
|
||||
let mut values = BTreeMap::new();
|
||||
for (key, value) in with {
|
||||
let key = match key {
|
||||
Value::String(s, ..) => s,
|
||||
let name = match key.node {
|
||||
Value::String(s, ..) => Identifier::from(s),
|
||||
v => {
|
||||
return Err((
|
||||
format!("$with key: {} is not a string.", v.inspect(span)?),
|
||||
@ -52,24 +54,45 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
||||
}
|
||||
};
|
||||
|
||||
config.insert(
|
||||
Spanned {
|
||||
node: key.into(),
|
||||
span,
|
||||
},
|
||||
value.span(span),
|
||||
)?;
|
||||
if values.contains_key(&name) {
|
||||
// todo: write test for this
|
||||
return Err((
|
||||
format!("The variable {name} was configured twice."),
|
||||
key.span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let (_, stmts) = parser.load_module(&url, &mut config)?;
|
||||
|
||||
Ok(stmts)
|
||||
} else {
|
||||
parser.parse_single_import(&url, span)
|
||||
}
|
||||
values.insert(name, ConfiguredValue::explicit(value, args.span()));
|
||||
}
|
||||
|
||||
fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
configuration = Configuration::explicit(values, args.span());
|
||||
}
|
||||
|
||||
let _configuration = Arc::new(RefCell::new(configuration));
|
||||
|
||||
let style_sheet = visitor.load_style_sheet(url.as_ref(), false, args.span())?;
|
||||
|
||||
visitor.visit_stylesheet(style_sheet)?;
|
||||
|
||||
// todo: support the $with argument to load-css
|
||||
// visitor.load_module(
|
||||
// url.as_ref(),
|
||||
// Some(Arc::clone(&configuration)),
|
||||
// true,
|
||||
// args.span(),
|
||||
// |visitor, module, stylesheet| {
|
||||
// // (*module).borrow()
|
||||
// Ok(())
|
||||
// },
|
||||
// )?;
|
||||
|
||||
// Visitor::assert_configuration_is_empty(&configuration, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let module = match args.get_err(0, "module")? {
|
||||
@ -84,11 +107,15 @@ fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
};
|
||||
|
||||
Ok(Value::Map(
|
||||
parser.modules.get(module.into(), args.span())?.functions(),
|
||||
(*(*visitor.env.modules)
|
||||
.borrow()
|
||||
.get(module.into(), args.span())?)
|
||||
.borrow()
|
||||
.functions(args.span()),
|
||||
))
|
||||
}
|
||||
|
||||
fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
||||
fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let module = match args.get_err(0, "module")? {
|
||||
@ -103,10 +130,66 @@ fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
||||
};
|
||||
|
||||
Ok(Value::Map(
|
||||
parser.modules.get(module.into(), args.span())?.variables(),
|
||||
(*(*visitor.env.modules)
|
||||
.borrow()
|
||||
.get(module.into(), args.span())?)
|
||||
.borrow()
|
||||
.variables(args.span()),
|
||||
))
|
||||
}
|
||||
|
||||
fn calc_args(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let calc = match args.get_err(0, "calc")? {
|
||||
Value::Calculation(calc) => calc,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let args = calc
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|arg| {
|
||||
Ok(match arg {
|
||||
CalculationArg::Number(num) => Value::Dimension(num),
|
||||
CalculationArg::Calculation(calc) => Value::Calculation(calc),
|
||||
CalculationArg::String(s) | CalculationArg::Interpolation(s) => {
|
||||
Value::String(s, QuoteKind::None)
|
||||
}
|
||||
CalculationArg::Operation { .. } => Value::String(
|
||||
serialize_calculation_arg(&arg, visitor.options, args.span())?,
|
||||
QuoteKind::None,
|
||||
),
|
||||
})
|
||||
})
|
||||
.collect::<SassResult<Vec<_>>>()?;
|
||||
|
||||
Ok(Value::List(args, ListSeparator::Comma, Brackets::None))
|
||||
}
|
||||
|
||||
fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
|
||||
args.max_args(1)?;
|
||||
|
||||
let calc = match args.get_err(0, "calc")? {
|
||||
Value::Calculation(calc) => calc,
|
||||
v => {
|
||||
return Err((
|
||||
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
|
||||
args.span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::String(calc.name.to_string(), QuoteKind::Quoted))
|
||||
}
|
||||
|
||||
pub(crate) fn declare(f: &mut Module) {
|
||||
f.insert_builtin("feature-exists", feature_exists);
|
||||
f.insert_builtin("inspect", inspect);
|
||||
@ -121,6 +204,8 @@ pub(crate) fn declare(f: &mut Module) {
|
||||
f.insert_builtin("module-functions", module_functions);
|
||||
f.insert_builtin("get-function", get_function);
|
||||
f.insert_builtin("call", call);
|
||||
f.insert_builtin("calc-args", calc_args);
|
||||
f.insert_builtin("calc-name", calc_name);
|
||||
|
||||
f.insert_builtin_mixin("load-css", load_css);
|
||||
}
|
||||
|
@ -1,18 +1,25 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, HashSet},
|
||||
fmt,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
atrule::mixin::{BuiltinMixin, Mixin},
|
||||
ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin},
|
||||
builtin::Builtin,
|
||||
common::{Identifier, QuoteKind},
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
scope::Scope,
|
||||
evaluate::{Environment, Visitor},
|
||||
selector::ExtensionStore,
|
||||
utils::{BaseMapView, MapView, MergedMapView, PublicMemberMapView},
|
||||
value::{SassFunction, SassMap, Value},
|
||||
};
|
||||
|
||||
use super::builtin_imports::QuoteKind;
|
||||
|
||||
mod color;
|
||||
mod list;
|
||||
mod map;
|
||||
@ -21,51 +28,89 @@ mod meta;
|
||||
mod selector;
|
||||
mod string;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Module {
|
||||
pub scope: Scope,
|
||||
|
||||
/// A module can itself import other modules
|
||||
pub modules: Modules,
|
||||
|
||||
/// Whether or not this module is builtin
|
||||
/// e.g. `"sass:math"`
|
||||
is_builtin: bool,
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ForwardedModule {
|
||||
inner: Arc<RefCell<Module>>,
|
||||
#[allow(dead_code)]
|
||||
forward_rule: AstForwardRule,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Modules(BTreeMap<Identifier, Module>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>);
|
||||
|
||||
impl ModuleConfig {
|
||||
/// Removes and returns element with name
|
||||
pub fn get(&mut self, name: Identifier) -> Option<Value> {
|
||||
self.0.remove(&name)
|
||||
}
|
||||
|
||||
/// If this structure is not empty at the end of
|
||||
/// an `@use`, we must throw an error
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, name: Spanned<Identifier>, value: Spanned<Value>) -> SassResult<()> {
|
||||
if self.0.insert(name.node, value.node).is_some() {
|
||||
Err((
|
||||
"The same variable may only be configured once.",
|
||||
name.span.merge(value.span),
|
||||
)
|
||||
.into())
|
||||
impl ForwardedModule {
|
||||
pub fn if_necessary(
|
||||
module: Arc<RefCell<Module>>,
|
||||
rule: AstForwardRule,
|
||||
) -> Arc<RefCell<Module>> {
|
||||
if rule.prefix.is_none()
|
||||
&& rule.shown_mixins_and_functions.is_none()
|
||||
&& rule.shown_variables.is_none()
|
||||
&& rule
|
||||
.hidden_mixins_and_functions
|
||||
.as_ref()
|
||||
.map_or(false, HashSet::is_empty)
|
||||
&& rule
|
||||
.hidden_variables
|
||||
.as_ref()
|
||||
.map_or(false, HashSet::is_empty)
|
||||
{
|
||||
module
|
||||
} else {
|
||||
Ok(())
|
||||
Arc::new(RefCell::new(Module::Forwarded(ForwardedModule {
|
||||
inner: module,
|
||||
forward_rule: rule,
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ModuleScope {
|
||||
pub variables: Arc<dyn MapView<Value = Value>>,
|
||||
pub mixins: Arc<dyn MapView<Value = Mixin>>,
|
||||
pub functions: Arc<dyn MapView<Value = SassFunction>>,
|
||||
}
|
||||
|
||||
impl ModuleScope {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||
mixins: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||
functions: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Module {
|
||||
Environment {
|
||||
scope: ModuleScope,
|
||||
#[allow(dead_code)]
|
||||
upstream: Vec<Module>,
|
||||
#[allow(dead_code)]
|
||||
extension_store: ExtensionStore,
|
||||
#[allow(dead_code)]
|
||||
env: Environment,
|
||||
},
|
||||
Builtin {
|
||||
scope: ModuleScope,
|
||||
},
|
||||
Forwarded(ForwardedModule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Modules(BTreeMap<Identifier, Arc<RefCell<Module>>>);
|
||||
|
||||
impl Modules {
|
||||
pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> {
|
||||
pub fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
name: Identifier,
|
||||
module: Arc<RefCell<Module>>,
|
||||
span: Span,
|
||||
) -> SassResult<()> {
|
||||
if self.0.contains_key(&name) {
|
||||
return Err((
|
||||
format!("There's already a module with namespace \"{}\".", name),
|
||||
@ -79,9 +124,9 @@ impl Modules {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, name: Identifier, span: Span) -> SassResult<&Module> {
|
||||
pub fn get(&self, name: Identifier, span: Span) -> SassResult<Arc<RefCell<Module>>> {
|
||||
match self.0.get(&name) {
|
||||
Some(v) => Ok(v),
|
||||
Some(v) => Ok(Arc::clone(v)),
|
||||
None => Err((
|
||||
format!(
|
||||
"There is no module with the namespace \"{}\".",
|
||||
@ -93,7 +138,11 @@ impl Modules {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, name: Identifier, span: Span) -> SassResult<&mut Module> {
|
||||
pub fn get_mut(
|
||||
&mut self,
|
||||
name: Identifier,
|
||||
span: Span,
|
||||
) -> SassResult<&mut Arc<RefCell<Module>>> {
|
||||
match self.0.get_mut(&name) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err((
|
||||
@ -106,153 +155,217 @@ impl Modules {
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
|
||||
fn member_map<V: fmt::Debug + Clone + 'static>(
|
||||
local: Arc<dyn MapView<Value = V>>,
|
||||
others: Vec<Arc<dyn MapView<Value = V>>>,
|
||||
) -> Arc<dyn MapView<Value = V>> {
|
||||
let local_map = PublicMemberMapView(local);
|
||||
|
||||
if others.is_empty() {
|
||||
return Arc::new(local_map);
|
||||
}
|
||||
|
||||
let mut all_maps: Vec<Arc<dyn MapView<Value = V>>> =
|
||||
others.into_iter().filter(|map| !map.is_empty()).collect();
|
||||
|
||||
all_maps.push(Arc::new(local_map));
|
||||
|
||||
// todo: potential optimization when all_maps.len() == 1
|
||||
Arc::new(MergedMapView::new(all_maps))
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub fn new_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<Identifier>) -> SassResult<&Value> {
|
||||
if name.node.as_str().starts_with('-') {
|
||||
return Err((
|
||||
"Private members can't be accessed from outside their modules.",
|
||||
name.span,
|
||||
)
|
||||
.into());
|
||||
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<Identifier>) -> SassResult<Value> {
|
||||
let scope = self.scope();
|
||||
|
||||
match scope.variables.get(name.node) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(("Undefined variable.", name.span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_var_no_err(&self, name: Identifier) -> Option<Value> {
|
||||
let scope = self.scope();
|
||||
|
||||
scope.variables.get(name)
|
||||
}
|
||||
|
||||
pub fn get_mixin_no_err(&self, name: Identifier) -> Option<Mixin> {
|
||||
let scope = self.scope();
|
||||
|
||||
scope.mixins.get(name)
|
||||
}
|
||||
|
||||
pub fn update_var(&mut self, name: Spanned<Identifier>, value: Value) -> SassResult<()> {
|
||||
if self.is_builtin {
|
||||
return Err(("Cannot modify built-in variable.", name.span).into());
|
||||
let scope = match self {
|
||||
Self::Builtin { .. } => {
|
||||
return Err(("Cannot modify built-in variable.", name.span).into())
|
||||
}
|
||||
Self::Environment { scope, .. } => scope.clone(),
|
||||
Self::Forwarded(forwarded) => (*forwarded.inner).borrow_mut().scope(),
|
||||
};
|
||||
|
||||
if scope.variables.insert(name.node, value).is_none() {
|
||||
return Err(("Undefined variable.", name.span).into());
|
||||
}
|
||||
|
||||
if name.node.as_str().starts_with('-') {
|
||||
return Err((
|
||||
"Private members can't be accessed from outside their modules.",
|
||||
name.span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if self.scope.insert_var(name.node, value).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(("Undefined variable.", name.span).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
|
||||
if name.node.as_str().starts_with('-') {
|
||||
return Err((
|
||||
"Private members can't be accessed from outside their modules.",
|
||||
name.span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let scope = self.scope();
|
||||
|
||||
match self.scope.mixins.get(&name.node) {
|
||||
Some(v) => Ok(v.clone()),
|
||||
match scope.mixins.get(name.node) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(("Undefined mixin.", name.span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) {
|
||||
self.scope.mixins.insert(name.into(), Mixin::Builtin(mixin));
|
||||
let scope = self.scope();
|
||||
|
||||
scope.mixins.insert(name.into(), Mixin::Builtin(mixin));
|
||||
}
|
||||
|
||||
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
|
||||
self.scope.vars.insert(name.into(), value);
|
||||
let ident = name.into();
|
||||
|
||||
let scope = self.scope();
|
||||
|
||||
scope.variables.insert(ident, value);
|
||||
}
|
||||
|
||||
pub fn get_fn(&self, name: Spanned<Identifier>) -> SassResult<Option<SassFunction>> {
|
||||
if name.node.as_str().starts_with('-') {
|
||||
return Err((
|
||||
"Private members can't be accessed from outside their modules.",
|
||||
name.span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||
let scope = self.scope();
|
||||
|
||||
Ok(self.scope.functions.get(&name.node).cloned())
|
||||
scope.functions.get(name)
|
||||
}
|
||||
|
||||
pub fn var_exists(&self, name: Identifier) -> bool {
|
||||
!name.as_str().starts_with('-') && self.scope.var_exists(name)
|
||||
let scope = self.scope();
|
||||
|
||||
scope.variables.get(name).is_some()
|
||||
}
|
||||
|
||||
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
||||
!name.as_str().starts_with('-') && self.scope.mixin_exists(name)
|
||||
let scope = self.scope();
|
||||
|
||||
scope.mixins.get(name).is_some()
|
||||
}
|
||||
|
||||
pub fn fn_exists(&self, name: Identifier) -> bool {
|
||||
!name.as_str().starts_with('-') && self.scope.fn_exists(name)
|
||||
let scope = self.scope();
|
||||
|
||||
scope.functions.get(name).is_some()
|
||||
}
|
||||
|
||||
pub fn insert_builtin(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
function: fn(CallArgs, &mut Parser) -> SassResult<Value>,
|
||||
function: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
|
||||
) {
|
||||
let ident = name.into();
|
||||
self.scope
|
||||
|
||||
let scope = match self {
|
||||
Self::Builtin { scope } => scope,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
scope
|
||||
.functions
|
||||
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
||||
}
|
||||
|
||||
pub fn functions(&self) -> SassMap {
|
||||
pub fn functions(&self, span: Span) -> SassMap {
|
||||
SassMap::new_with(
|
||||
self.scope
|
||||
self.scope()
|
||||
.functions
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
Value::String(key.to_string(), QuoteKind::Quoted),
|
||||
Value::FunctionRef(value.clone()),
|
||||
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
|
||||
Value::FunctionRef(value),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(Value, Value)>>(),
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn variables(&self) -> SassMap {
|
||||
pub fn variables(&self, span: Span) -> SassMap {
|
||||
SassMap::new_with(
|
||||
self.scope
|
||||
.vars
|
||||
self.scope()
|
||||
.variables
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
Value::String(key.to_string(), QuoteKind::Quoted),
|
||||
value.clone(),
|
||||
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
|
||||
value,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(Value, Value)>>(),
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn new_from_scope(scope: Scope, modules: Modules, is_builtin: bool) -> Self {
|
||||
Module {
|
||||
scope,
|
||||
modules,
|
||||
is_builtin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn declare_module_color() -> Module {
|
||||
|
397
src/color/mod.rs
397
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<Hsla>,
|
||||
repr: String,
|
||||
pub format: ColorFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum ColorFormat {
|
||||
Rgb,
|
||||
Hsl,
|
||||
/// Literal string from source text. Either a named color like `red` or a hex color
|
||||
// todo: make this is a span and lookup text from codemap
|
||||
Literal(String),
|
||||
/// Use the most appropriate format
|
||||
Infer,
|
||||
}
|
||||
|
||||
impl PartialEq for Color {
|
||||
@ -48,12 +53,12 @@ impl Color {
|
||||
green: Number,
|
||||
blue: Number,
|
||||
alpha: Number,
|
||||
repr: String,
|
||||
format: ColorFormat,
|
||||
) -> Color {
|
||||
Color {
|
||||
rgba: Rgba::new(red, green, blue, alpha),
|
||||
hsla: None,
|
||||
repr,
|
||||
format,
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,12 +68,11 @@ impl Color {
|
||||
blue: Number,
|
||||
alpha: Number,
|
||||
hsla: Hsla,
|
||||
repr: String,
|
||||
) -> Color {
|
||||
Color {
|
||||
rgba: Rgba::new(red, green, blue, alpha),
|
||||
hsla: Some(hsla),
|
||||
repr,
|
||||
format: ColorFormat::Infer,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,17 +88,17 @@ struct Rgba {
|
||||
impl PartialEq for Rgba {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.red != other.red
|
||||
&& !(self.red >= Number::from(255) && other.red >= Number::from(255))
|
||||
&& !(self.red >= Number::from(255.0) && other.red >= Number::from(255.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.green != other.green
|
||||
&& !(self.green >= Number::from(255) && other.green >= Number::from(255))
|
||||
&& !(self.green >= Number::from(255.0) && other.green >= Number::from(255.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.blue != other.blue
|
||||
&& !(self.blue >= Number::from(255) && other.blue >= Number::from(255))
|
||||
&& !(self.blue >= Number::from(255.0) && other.blue >= Number::from(255.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -120,7 +124,7 @@ impl Rgba {
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> Number {
|
||||
self.alpha.clone()
|
||||
self.alpha
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,29 +147,29 @@ impl Hsla {
|
||||
}
|
||||
|
||||
pub fn hue(&self) -> Number {
|
||||
self.hue.clone()
|
||||
self.hue
|
||||
}
|
||||
|
||||
pub fn saturation(&self) -> Number {
|
||||
self.saturation.clone()
|
||||
self.saturation
|
||||
}
|
||||
|
||||
pub fn luminance(&self) -> Number {
|
||||
self.luminance.clone()
|
||||
self.luminance
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> Number {
|
||||
self.alpha.clone()
|
||||
self.alpha
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA color functions
|
||||
impl Color {
|
||||
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self {
|
||||
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, format: String) -> Self {
|
||||
Color {
|
||||
rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()),
|
||||
hsla: None,
|
||||
repr,
|
||||
format: ColorFormat::Literal(format),
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,50 +181,62 @@ impl Color {
|
||||
mut blue: Number,
|
||||
mut alpha: Number,
|
||||
) -> Self {
|
||||
red = red.clamp(0, 255);
|
||||
green = green.clamp(0, 255);
|
||||
blue = blue.clamp(0, 255);
|
||||
alpha = alpha.clamp(0, 1);
|
||||
red = red.clamp(0.0, 255.0);
|
||||
green = green.clamp(0.0, 255.0);
|
||||
blue = blue.clamp(0.0, 255.0);
|
||||
alpha = alpha.clamp(0.0, 1.0);
|
||||
|
||||
let repr = repr(&red, &green, &blue, &alpha);
|
||||
Color::new_rgba(red, green, blue, alpha, repr)
|
||||
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
|
||||
}
|
||||
|
||||
pub fn from_rgba_fn(
|
||||
mut red: Number,
|
||||
mut green: Number,
|
||||
mut blue: Number,
|
||||
mut alpha: Number,
|
||||
) -> Self {
|
||||
red = red.clamp(0.0, 255.0);
|
||||
green = green.clamp(0.0, 255.0);
|
||||
blue = blue.clamp(0.0, 255.0);
|
||||
alpha = alpha.clamp(0.0, 1.0);
|
||||
|
||||
Color::new_rgba(red, green, blue, alpha, ColorFormat::Rgb)
|
||||
}
|
||||
|
||||
pub fn red(&self) -> Number {
|
||||
self.rgba.red.clone().round()
|
||||
self.rgba.red.round()
|
||||
}
|
||||
|
||||
pub fn blue(&self) -> Number {
|
||||
self.rgba.blue.clone().round()
|
||||
self.rgba.blue.round()
|
||||
}
|
||||
|
||||
pub fn green(&self) -> Number {
|
||||
self.rgba.green.clone().round()
|
||||
self.rgba.green.round()
|
||||
}
|
||||
|
||||
/// Mix two colors together with weight
|
||||
/// Algorithm adapted from
|
||||
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
|
||||
pub fn mix(self, other: &Color, weight: Number) -> Self {
|
||||
let weight = weight.clamp(0, 100);
|
||||
let normalized_weight = weight.clone() * Number::from(2) - Number::one();
|
||||
let weight = weight.clamp(0.0, 100.0);
|
||||
let normalized_weight = weight * Number::from(2.0) - Number::one();
|
||||
let alpha_distance = self.alpha() - other.alpha();
|
||||
|
||||
let combined_weight1 =
|
||||
if normalized_weight.clone() * alpha_distance.clone() == Number::from(-1) {
|
||||
let combined_weight1 = if normalized_weight * alpha_distance == Number::from(-1) {
|
||||
normalized_weight
|
||||
} else {
|
||||
(normalized_weight.clone() + alpha_distance.clone())
|
||||
(normalized_weight + alpha_distance)
|
||||
/ (Number::one() + normalized_weight * alpha_distance)
|
||||
};
|
||||
let weight1 = (combined_weight1 + Number::one()) / Number::from(2);
|
||||
let weight2 = Number::one() - weight1.clone();
|
||||
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,92 +378,67 @@ impl Color {
|
||||
|
||||
pub fn saturate(&self, amount: Number) -> Self {
|
||||
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
||||
Color::from_hsla(hue, saturation + amount, luminance, alpha)
|
||||
Color::from_hsla(hue, (saturation + amount).clamp(0.0, 1.0), luminance, alpha)
|
||||
}
|
||||
|
||||
pub fn desaturate(&self, amount: Number) -> Self {
|
||||
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
||||
Color::from_hsla(hue, saturation - amount, luminance, alpha)
|
||||
Color::from_hsla(hue, (saturation - amount).clamp(0.0, 1.0), luminance, alpha)
|
||||
}
|
||||
|
||||
pub fn from_hsla_fn(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self {
|
||||
let mut color = Self::from_hsla(hue, saturation, luminance, alpha);
|
||||
color.format = ColorFormat::Hsl;
|
||||
color
|
||||
}
|
||||
|
||||
/// Create RGBA representation from HSLA values
|
||||
pub fn from_hsla(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self {
|
||||
let mut hue = if hue >= Number::from(360) {
|
||||
hue % Number::from(360)
|
||||
} else if hue < Number::from(-360) {
|
||||
Number::from(360) + hue % Number::from(360)
|
||||
} else if hue.is_negative() {
|
||||
Number::from(360) + hue.clamp(-360, 360)
|
||||
} else {
|
||||
hue
|
||||
};
|
||||
|
||||
let saturation = saturation.clamp(0, 1);
|
||||
let luminance = luminance.clamp(0, 1);
|
||||
let alpha = alpha.clamp(0, 1);
|
||||
|
||||
pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self {
|
||||
let hsla = Hsla::new(
|
||||
hue.clone(),
|
||||
saturation.clone(),
|
||||
luminance.clone(),
|
||||
alpha.clone(),
|
||||
hue,
|
||||
saturation.clamp(0.0, 1.0),
|
||||
lightness.clamp(0.0, 1.0),
|
||||
alpha,
|
||||
);
|
||||
|
||||
if saturation.is_zero() {
|
||||
let val = luminance * Number::from(255);
|
||||
let repr = repr(&val, &val, &val, &alpha);
|
||||
return Color::new_hsla(val.clone(), val.clone(), val, alpha, hsla, repr);
|
||||
}
|
||||
let scaled_hue = hue.0 / 360.0;
|
||||
let scaled_saturation = saturation.0.clamp(0.0, 1.0);
|
||||
let scaled_lightness = lightness.0.clamp(0.0, 1.0);
|
||||
|
||||
let temporary_1 = if luminance < Number::small_ratio(1, 2) {
|
||||
luminance.clone() * (Number::one() + saturation)
|
||||
let m2 = if scaled_lightness <= 0.5 {
|
||||
scaled_lightness * (scaled_saturation + 1.0)
|
||||
} else {
|
||||
luminance.clone() + saturation.clone() - luminance.clone() * saturation
|
||||
scaled_lightness.mul_add(-scaled_saturation, scaled_lightness + scaled_saturation)
|
||||
};
|
||||
let temporary_2 = Number::from(2) * luminance - temporary_1.clone();
|
||||
hue /= Number::from(360);
|
||||
let mut temporary_r = hue.clone() + Number::small_ratio(1, 3);
|
||||
let mut temporary_g = hue.clone();
|
||||
let mut temporary_b = hue - Number::small_ratio(1, 3);
|
||||
|
||||
macro_rules! clamp_temp {
|
||||
($temp:ident) => {
|
||||
if $temp > Number::one() {
|
||||
$temp -= Number::one();
|
||||
} else if $temp.is_negative() {
|
||||
$temp += Number::one();
|
||||
}
|
||||
};
|
||||
let m1 = scaled_lightness.mul_add(2.0, -m2);
|
||||
|
||||
let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0);
|
||||
let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0);
|
||||
let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0);
|
||||
|
||||
Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla)
|
||||
}
|
||||
|
||||
clamp_temp!(temporary_r);
|
||||
clamp_temp!(temporary_g);
|
||||
clamp_temp!(temporary_b);
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
temp2.clone()
|
||||
m1
|
||||
}
|
||||
}
|
||||
|
||||
let red = channel(temporary_r, &temporary_1, &temporary_2);
|
||||
let green = channel(temporary_g, &temporary_1, &temporary_2);
|
||||
let blue = channel(temporary_b, &temporary_1, &temporary_2);
|
||||
|
||||
let repr = repr(&red, &green, &blue, &alpha);
|
||||
Color::new_hsla(red, green, blue, alpha, hsla, repr)
|
||||
}
|
||||
|
||||
pub fn invert(&self, weight: Number) -> Self {
|
||||
if weight.is_zero() {
|
||||
return self.clone();
|
||||
@ -456,9 +447,8 @@ impl Color {
|
||||
let red = Number::from(u8::max_value()) - self.red();
|
||||
let green = Number::from(u8::max_value()) - self.green();
|
||||
let blue = Number::from(u8::max_value()) - self.blue();
|
||||
let repr = repr(&red, &green, &blue, &self.alpha());
|
||||
|
||||
let inverse = Color::new_rgba(red, green, blue, self.alpha(), repr);
|
||||
let inverse = Color::new_rgba(red, green, blue, self.alpha(), ColorFormat::Infer);
|
||||
|
||||
inverse.mix(self, weight)
|
||||
}
|
||||
@ -475,7 +465,7 @@ impl Color {
|
||||
pub fn alpha(&self) -> Number {
|
||||
let a = self.rgba.alpha();
|
||||
if a > Number::one() {
|
||||
a / Number::from(255)
|
||||
a / Number::from(255.0)
|
||||
} else {
|
||||
a
|
||||
}
|
||||
@ -505,108 +495,41 @@ impl Color {
|
||||
impl Color {
|
||||
pub fn to_ie_hex_str(&self) -> String {
|
||||
format!(
|
||||
"#{:X}{:X}{:X}{:X}",
|
||||
(self.alpha() * Number::from(255)).round().to_integer(),
|
||||
self.red().to_integer(),
|
||||
self.green().to_integer(),
|
||||
self.blue().to_integer()
|
||||
"#{:02X}{:02X}{:02X}{:02X}",
|
||||
fuzzy_round(self.alpha().0 * 255.0) as u8,
|
||||
self.red().0 as u8,
|
||||
self.green().0 as u8,
|
||||
self.blue().0 as u8
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// HWB color functions
|
||||
impl Color {
|
||||
pub fn from_hwb(
|
||||
mut hue: Number,
|
||||
mut white: Number,
|
||||
mut black: Number,
|
||||
mut alpha: Number,
|
||||
) -> Color {
|
||||
hue %= Number::from(360);
|
||||
hue /= Number::from(360);
|
||||
white /= Number::from(100);
|
||||
black /= Number::from(100);
|
||||
alpha = alpha.clamp(Number::zero(), Number::one());
|
||||
pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color {
|
||||
let hue = Number(hue.rem_euclid(360.0) / 360.0);
|
||||
let mut scaled_white = white.0 / 100.0;
|
||||
let mut scaled_black = black.0 / 100.0;
|
||||
alpha = alpha.clamp(0.0, 1.0);
|
||||
|
||||
let white_black_sum = white.clone() + black.clone();
|
||||
let white_black_sum = scaled_white + scaled_black;
|
||||
|
||||
if white_black_sum > Number::one() {
|
||||
white /= white_black_sum.clone();
|
||||
black /= white_black_sum;
|
||||
if white_black_sum > 1.0 {
|
||||
scaled_white /= white_black_sum;
|
||||
scaled_black /= white_black_sum;
|
||||
}
|
||||
|
||||
let factor = Number::one() - white.clone() - black;
|
||||
let factor = 1.0 - scaled_white - scaled_black;
|
||||
|
||||
fn channel(m1: Number, m2: Number, mut hue: Number) -> Number {
|
||||
if hue < Number::zero() {
|
||||
hue += Number::one();
|
||||
}
|
||||
|
||||
if hue > Number::one() {
|
||||
hue -= Number::one();
|
||||
}
|
||||
|
||||
if hue < Number::small_ratio(1, 6) {
|
||||
m1.clone() + (m2 - m1) * hue * Number::from(6)
|
||||
} else if hue < Number::small_ratio(1, 2) {
|
||||
m2
|
||||
} else if hue < Number::small_ratio(2, 3) {
|
||||
m1.clone() + (m2 - m1) * (Number::small_ratio(2, 3) - hue) * Number::from(6)
|
||||
} else {
|
||||
m1
|
||||
}
|
||||
}
|
||||
|
||||
let to_rgb = |hue: Number| -> Number {
|
||||
let channel =
|
||||
channel(Number::zero(), Number::one(), hue) * factor.clone() + white.clone();
|
||||
channel * Number::from(255)
|
||||
let to_rgb = |hue: f64| -> Number {
|
||||
let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white);
|
||||
Number(fuzzy_round(channel * 255.0))
|
||||
};
|
||||
|
||||
let red = to_rgb(hue.clone() + Number::small_ratio(1, 3));
|
||||
let green = to_rgb(hue.clone());
|
||||
let blue = to_rgb(hue - Number::small_ratio(1, 3));
|
||||
let red = to_rgb(hue.0 + 1.0 / 3.0);
|
||||
let green = to_rgb(hue.0);
|
||||
let blue = to_rgb(hue.0 - 1.0 / 3.0);
|
||||
|
||||
let repr = repr(&red, &green, &blue, &alpha);
|
||||
|
||||
Color::new_rgba(red, green, blue, alpha, repr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the proper representation from RGBA values
|
||||
fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
|
||||
fn into_u8(channel: &Number) -> u8 {
|
||||
if channel > &Number::from(255) {
|
||||
255_u8
|
||||
} else if channel.is_negative() {
|
||||
0_u8
|
||||
} else {
|
||||
channel.round().to_integer().to_u8().unwrap_or(255)
|
||||
}
|
||||
}
|
||||
|
||||
let red_u8 = into_u8(red);
|
||||
let green_u8 = into_u8(green);
|
||||
let blue_u8 = into_u8(blue);
|
||||
|
||||
if alpha < &Number::one() {
|
||||
format!(
|
||||
"rgba({}, {}, {}, {})",
|
||||
red_u8,
|
||||
green_u8,
|
||||
blue_u8,
|
||||
// todo: is_compressed
|
||||
alpha.inspect()
|
||||
)
|
||||
} else if let Some(c) = NAMED_COLORS.get_by_rgba([red_u8, green_u8, blue_u8]) {
|
||||
(*c).to_owned()
|
||||
} else {
|
||||
format!("#{:0>2x}{:0>2x}{:0>2x}", red_u8, green_u8, blue_u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.repr)
|
||||
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
|
||||
}
|
||||
}
|
||||
|
117
src/common.rs
117
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 {
|
||||
impl BinaryOp {
|
||||
pub fn precedence(self) -> u8 {
|
||||
match self {
|
||||
Self::SingleEq => 0,
|
||||
Self::Or => 1,
|
||||
Self::And => 2,
|
||||
Self::Equal | Self::NotEqual => 3,
|
||||
Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan | Self::LessThanEqual => 4,
|
||||
Self::Plus | Self::Minus => 5,
|
||||
Self::Mul | Self::Div | Self::Rem => 6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BinaryOp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Op {
|
||||
/// Get order of precedence for an operator
|
||||
///
|
||||
/// Higher numbers are evaluated first.
|
||||
/// Do not rely on the number itself, but rather the size relative to other numbers
|
||||
///
|
||||
/// If precedence is equal, the leftmost operation is evaluated first
|
||||
pub fn precedence(self) -> usize {
|
||||
match self {
|
||||
Self::And | Self::Or | Self::Not => 0,
|
||||
Self::Equal
|
||||
| Self::NotEqual
|
||||
| Self::GreaterThan
|
||||
| Self::GreaterThanEqual
|
||||
| Self::LessThan
|
||||
| Self::LessThanEqual => 1,
|
||||
Self::Plus | Self::Minus => 2,
|
||||
Self::Mul | Self::Div | Self::Rem => 3,
|
||||
BinaryOp::SingleEq => write!(f, "="),
|
||||
BinaryOp::Equal => write!(f, "=="),
|
||||
BinaryOp::NotEqual => write!(f, "!="),
|
||||
BinaryOp::GreaterThanEqual => write!(f, ">="),
|
||||
BinaryOp::LessThanEqual => write!(f, "<="),
|
||||
BinaryOp::GreaterThan => write!(f, ">"),
|
||||
BinaryOp::LessThan => write!(f, "<"),
|
||||
BinaryOp::Plus => write!(f, "+"),
|
||||
BinaryOp::Minus => write!(f, "-"),
|
||||
BinaryOp::Mul => write!(f, "*"),
|
||||
BinaryOp::Div => write!(f, "/"),
|
||||
BinaryOp::Rem => write!(f, "%"),
|
||||
BinaryOp::And => write!(f, "and"),
|
||||
BinaryOp::Or => write!(f, "or"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,31 +86,47 @@ pub(crate) enum Brackets {
|
||||
Bracketed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, Eq)]
|
||||
pub(crate) enum ListSeparator {
|
||||
Space,
|
||||
Comma,
|
||||
Slash,
|
||||
Undecided,
|
||||
}
|
||||
|
||||
impl PartialEq for ListSeparator {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Space | Self::Undecided, Self::Space | Self::Undecided) => true,
|
||||
(Self::Comma, Self::Comma) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListSeparator {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Space => " ",
|
||||
Self::Space | Self::Undecided => " ",
|
||||
Self::Comma => ", ",
|
||||
Self::Slash => " / ",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_compressed_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Space => " ",
|
||||
Self::Space | Self::Undecided => " ",
|
||||
Self::Comma => ",",
|
||||
Self::Slash => "/",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Space => "space",
|
||||
Self::Space | Self::Undecided => "space",
|
||||
Self::Comma => "comma",
|
||||
Self::Slash => "slash",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -119,9 +136,17 @@ impl ListSeparator {
|
||||
///
|
||||
/// This struct protects that invariant by normalizing all
|
||||
/// underscores into hypens.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)]
|
||||
pub(crate) struct Identifier(InternedString);
|
||||
|
||||
impl fmt::Debug for Identifier {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Identifier")
|
||||
.field(&self.0.to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier {
|
||||
fn from_str(s: &str) -> Self {
|
||||
if s.contains('_') {
|
||||
@ -130,6 +155,10 @@ impl Identifier {
|
||||
Identifier(InternedString::get_or_intern(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
!self.as_str().starts_with('-')
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Identifier {
|
||||
|
112
src/context_flags.rs
Normal file
112
src/context_flags.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::ops::{BitAnd, BitOr, BitOrAssign};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct ContextFlags(pub u16);
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct ContextFlag(u16);
|
||||
|
||||
impl ContextFlags {
|
||||
pub const IN_MIXIN: ContextFlag = ContextFlag(1);
|
||||
pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1);
|
||||
pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2);
|
||||
pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3);
|
||||
pub const FOUND_CONTENT_RULE: ContextFlag = ContextFlag(1 << 4);
|
||||
pub const IN_STYLE_RULE: ContextFlag = ContextFlag(1 << 5);
|
||||
pub const IN_UNKNOWN_AT_RULE: ContextFlag = ContextFlag(1 << 6);
|
||||
pub const IN_CONTENT_BLOCK: ContextFlag = ContextFlag(1 << 7);
|
||||
pub const IS_USE_ALLOWED: ContextFlag = ContextFlag(1 << 8);
|
||||
pub const IN_PARENS: ContextFlag = ContextFlag(1 << 9);
|
||||
pub const AT_ROOT_EXCLUDING_STYLE_RULE: ContextFlag = ContextFlag(1 << 10);
|
||||
pub const IN_SUPPORTS_DECLARATION: ContextFlag = ContextFlag(1 << 11);
|
||||
pub const IN_SEMI_GLOBAL_SCOPE: ContextFlag = ContextFlag(1 << 12);
|
||||
|
||||
pub const fn empty() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn unset(&mut self, flag: ContextFlag) {
|
||||
self.0 &= !flag.0;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, flag: ContextFlag, v: bool) {
|
||||
if v {
|
||||
self.0 |= flag.0;
|
||||
} else {
|
||||
self.unset(flag);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_mixin(self) -> bool {
|
||||
(self.0 & Self::IN_MIXIN) != 0
|
||||
}
|
||||
|
||||
pub fn in_function(self) -> bool {
|
||||
(self.0 & Self::IN_FUNCTION) != 0
|
||||
}
|
||||
|
||||
pub fn in_control_flow(self) -> bool {
|
||||
(self.0 & Self::IN_CONTROL_FLOW) != 0
|
||||
}
|
||||
|
||||
pub fn in_keyframes(self) -> bool {
|
||||
(self.0 & Self::IN_KEYFRAMES) != 0
|
||||
}
|
||||
|
||||
pub fn in_style_rule(self) -> bool {
|
||||
(self.0 & Self::IN_STYLE_RULE) != 0
|
||||
}
|
||||
|
||||
pub fn in_unknown_at_rule(self) -> bool {
|
||||
(self.0 & Self::IN_UNKNOWN_AT_RULE) != 0
|
||||
}
|
||||
|
||||
pub fn in_content_block(self) -> bool {
|
||||
(self.0 & Self::IN_CONTENT_BLOCK) != 0
|
||||
}
|
||||
|
||||
pub fn in_parens(self) -> bool {
|
||||
(self.0 & Self::IN_PARENS) != 0
|
||||
}
|
||||
|
||||
pub fn at_root_excluding_style_rule(self) -> bool {
|
||||
(self.0 & Self::AT_ROOT_EXCLUDING_STYLE_RULE) != 0
|
||||
}
|
||||
|
||||
pub fn in_supports_declaration(self) -> bool {
|
||||
(self.0 & Self::IN_SUPPORTS_DECLARATION) != 0
|
||||
}
|
||||
|
||||
pub fn in_semi_global_scope(self) -> bool {
|
||||
(self.0 & Self::IN_SEMI_GLOBAL_SCOPE) != 0
|
||||
}
|
||||
|
||||
pub fn found_content_rule(self) -> bool {
|
||||
(self.0 & Self::FOUND_CONTENT_RULE) != 0
|
||||
}
|
||||
|
||||
pub fn is_use_allowed(self) -> bool {
|
||||
(self.0 & Self::IS_USE_ALLOWED) != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAnd<ContextFlag> for u16 {
|
||||
type Output = Self;
|
||||
#[inline]
|
||||
fn bitand(self, rhs: ContextFlag) -> Self::Output {
|
||||
self & rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr<ContextFlag> for ContextFlags {
|
||||
type Output = Self;
|
||||
fn bitor(self, rhs: ContextFlag) -> Self::Output {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOrAssign<ContextFlag> for ContextFlags {
|
||||
fn bitor_assign(&mut self, rhs: ContextFlag) {
|
||||
self.0 |= rhs.0;
|
||||
}
|
||||
}
|
13
src/error.rs
13
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::<String>()
|
||||
)?;
|
||||
writeln!(f, "{}{}", padding, fourth_bar)?;
|
||||
|
||||
if unicode {
|
||||
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
|
||||
} else {
|
||||
writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
603
src/evaluate/bin_op.rs
Normal file
603
src/evaluate/bin_op.rs
Normal file
@ -0,0 +1,603 @@
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use codemap::Span;
|
||||
|
||||
use crate::{
|
||||
common::{BinaryOp, QuoteKind},
|
||||
error::SassResult,
|
||||
serializer::serialize_number,
|
||||
unit::Unit,
|
||||
value::{SassNumber, Value},
|
||||
Options,
|
||||
};
|
||||
|
||||
pub(crate) fn add(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
|
||||
Ok(match left {
|
||||
Value::Calculation(..) => match right {
|
||||
Value::String(s, quotes) => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
s
|
||||
),
|
||||
quotes,
|
||||
),
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} + {}\".",
|
||||
left.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Value::Map(..) | Value::FunctionRef(..) => {
|
||||
return Err((
|
||||
format!("{} isn't a valid CSS value.", left.inspect(span)?),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::True | Value::False => match right {
|
||||
Value::String(s, QuoteKind::Quoted) => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
s
|
||||
),
|
||||
QuoteKind::Quoted,
|
||||
),
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
Value::Null => match right {
|
||||
Value::Null => Value::Null,
|
||||
_ => Value::String(
|
||||
right
|
||||
.to_css_string(span, options.is_compressed())?
|
||||
.into_owned(),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => match right {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
if !unit.comparable(&unit2) {
|
||||
return Err(
|
||||
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
|
||||
);
|
||||
}
|
||||
if unit == unit2 {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num + num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
} else if unit == Unit::None {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num + num2,
|
||||
unit: unit2,
|
||||
as_slash: None,
|
||||
})
|
||||
} else if unit2 == Unit::None {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num + num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
} else {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num + num2.convert(&unit2, &unit),
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Value::String(s, q) => Value::String(
|
||||
format!("{}{}{}", num.to_string(options.is_compressed()), unit, s),
|
||||
q,
|
||||
),
|
||||
Value::Null => Value::String(
|
||||
format!("{}{}", num.to_string(options.is_compressed()), unit),
|
||||
QuoteKind::None,
|
||||
),
|
||||
Value::True | Value::False | Value::List(..) | Value::ArgList(..) => Value::String(
|
||||
format!(
|
||||
"{}{}{}",
|
||||
num.to_string(options.is_compressed()),
|
||||
unit,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
Value::Map(..) | Value::FunctionRef(..) => {
|
||||
return Err((
|
||||
format!("{} isn't a valid CSS value.", right.inspect(span)?),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Color(..) | Value::Calculation(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{}{} + {}\".",
|
||||
num.inspect(),
|
||||
unit,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
c @ Value::Color(..) => match right {
|
||||
// todo: we really cant add to any other types?
|
||||
Value::String(..) | Value::Null | Value::List(..) => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
c.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?,
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} + {}\".",
|
||||
c.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Value::String(text, quotes) => match right {
|
||||
Value::String(text2, ..) => Value::String(text + &text2, quotes),
|
||||
_ => Value::String(
|
||||
text + &right.to_css_string(span, options.is_compressed())?,
|
||||
quotes,
|
||||
),
|
||||
},
|
||||
Value::List(..) | Value::ArgList(..) => match right {
|
||||
Value::String(s, q) => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
s
|
||||
),
|
||||
q,
|
||||
),
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sub(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
|
||||
Ok(match left {
|
||||
Value::Calculation(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} - {}\".",
|
||||
left.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Null => Value::String(
|
||||
format!("-{}", right.to_css_string(span, options.is_compressed())?),
|
||||
QuoteKind::None,
|
||||
),
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => match right {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
if !unit.comparable(&unit2) {
|
||||
return Err(
|
||||
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
|
||||
);
|
||||
}
|
||||
if unit == unit2 {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num - num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
} else if unit == Unit::None {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num - num2,
|
||||
unit: unit2,
|
||||
as_slash: None,
|
||||
})
|
||||
} else if unit2 == Unit::None {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num - num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
} else {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num - num2.convert(&unit2, &unit),
|
||||
unit,
|
||||
as_slash: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Value::List(..)
|
||||
| Value::String(..)
|
||||
| Value::True
|
||||
| Value::False
|
||||
| Value::ArgList(..) => Value::String(
|
||||
format!(
|
||||
"{}{}-{}",
|
||||
num.to_string(options.is_compressed()),
|
||||
unit,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
Value::Map(..) | Value::FunctionRef(..) => {
|
||||
return Err((
|
||||
format!("{} isn't a valid CSS value.", right.inspect(span)?),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Color(..) | Value::Calculation(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{}{} - {}\".",
|
||||
num.inspect(),
|
||||
unit,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Value::Null => Value::String(
|
||||
format!("{}{}-", num.to_string(options.is_compressed()), unit),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
c @ Value::Color(..) => match right {
|
||||
Value::Dimension(SassNumber { .. }) | Value::Color(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} - {}\".",
|
||||
c.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}-{}",
|
||||
c.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
Value::String(..) => Value::String(
|
||||
format!(
|
||||
"{}-{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
// todo: can be greatly simplified
|
||||
_ => match right {
|
||||
Value::String(s, q) => Value::String(
|
||||
format!(
|
||||
"{}-{}{}{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
q,
|
||||
s,
|
||||
q
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
Value::Null => Value::String(
|
||||
format!("{}-", left.to_css_string(span, options.is_compressed())?),
|
||||
QuoteKind::None,
|
||||
),
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}-{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn mul(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
|
||||
Ok(match left {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: _,
|
||||
}) => match right {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
if unit2 == Unit::None {
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: num * num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
|
||||
let n = SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: None,
|
||||
} * SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: None,
|
||||
};
|
||||
|
||||
Value::Dimension(n)
|
||||
}
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{}{} * {}\".",
|
||||
num.inspect(),
|
||||
unit,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} * {}\".",
|
||||
left.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn cmp(
|
||||
left: &Value,
|
||||
right: &Value,
|
||||
options: &Options,
|
||||
span: Span,
|
||||
op: BinaryOp,
|
||||
) -> SassResult<Value> {
|
||||
let ordering = match left.cmp(right, span, op)? {
|
||||
Some(ord) => ord,
|
||||
None => return Ok(Value::False),
|
||||
};
|
||||
|
||||
Ok(match op {
|
||||
BinaryOp::GreaterThan => match ordering {
|
||||
Ordering::Greater => Value::True,
|
||||
Ordering::Less | Ordering::Equal => Value::False,
|
||||
},
|
||||
BinaryOp::GreaterThanEqual => match ordering {
|
||||
Ordering::Greater | Ordering::Equal => Value::True,
|
||||
Ordering::Less => Value::False,
|
||||
},
|
||||
BinaryOp::LessThan => match ordering {
|
||||
Ordering::Less => Value::True,
|
||||
Ordering::Greater | Ordering::Equal => Value::False,
|
||||
},
|
||||
BinaryOp::LessThanEqual => match ordering {
|
||||
Ordering::Less | Ordering::Equal => Value::True,
|
||||
Ordering::Greater => Value::False,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn single_eq(
|
||||
left: &Value,
|
||||
right: &Value,
|
||||
options: &Options,
|
||||
span: Span,
|
||||
) -> SassResult<Value> {
|
||||
Ok(Value::String(
|
||||
format!(
|
||||
"{}={}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
))
|
||||
}
|
||||
|
||||
// todo: simplify matching
|
||||
pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
|
||||
Ok(match left {
|
||||
Value::Dimension(SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: as_slash1,
|
||||
}) => match right {
|
||||
Value::Dimension(SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: as_slash2,
|
||||
}) => {
|
||||
if unit2 == Unit::None {
|
||||
return Ok(Value::Dimension(SassNumber {
|
||||
num: num / num2,
|
||||
unit,
|
||||
as_slash: None,
|
||||
}));
|
||||
}
|
||||
|
||||
let n = SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: None,
|
||||
} / SassNumber {
|
||||
num: num2,
|
||||
unit: unit2,
|
||||
as_slash: None,
|
||||
};
|
||||
|
||||
Value::Dimension(n)
|
||||
}
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}/{}",
|
||||
serialize_number(
|
||||
&SassNumber {
|
||||
num,
|
||||
unit,
|
||||
as_slash: as_slash1
|
||||
},
|
||||
options,
|
||||
span
|
||||
)?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
c @ Value::Color(..) => match right {
|
||||
Value::Dimension(SassNumber { .. }) | Value::Color(..) => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} / {}\".",
|
||||
c.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}/{}",
|
||||
c.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
},
|
||||
_ => Value::String(
|
||||
format!(
|
||||
"{}/{}",
|
||||
left.to_css_string(span, options.is_compressed())?,
|
||||
right.to_css_string(span, options.is_compressed())?
|
||||
),
|
||||
QuoteKind::None,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn rem(left: Value, right: Value, options: &Options, span: Span) -> SassResult<Value> {
|
||||
Ok(match left {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: _,
|
||||
}) => match right {
|
||||
Value::Dimension(SassNumber {
|
||||
num: n2,
|
||||
unit: u2,
|
||||
as_slash: _,
|
||||
}) => {
|
||||
if !u.comparable(&u2) {
|
||||
return Err((format!("Incompatible units {} and {}.", u, u2), span).into());
|
||||
}
|
||||
|
||||
let new_num = n % n2.convert(&u2, &u);
|
||||
let new_unit = if u == u2 {
|
||||
u
|
||||
} else if u == Unit::None {
|
||||
u2
|
||||
} else {
|
||||
u
|
||||
};
|
||||
Value::Dimension(SassNumber {
|
||||
num: new_num,
|
||||
unit: new_unit,
|
||||
as_slash: None,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let val = Value::Dimension(SassNumber {
|
||||
num: n,
|
||||
unit: u,
|
||||
as_slash: None,
|
||||
})
|
||||
.inspect(span)?;
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} % {}\".",
|
||||
val,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"Undefined operation \"{} % {}\".",
|
||||
left.inspect(span)?,
|
||||
right.inspect(span)?
|
||||
),
|
||||
span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
}
|
154
src/evaluate/css_tree.rs
Normal file
154
src/evaluate/css_tree.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
collections::BTreeMap,
|
||||
};
|
||||
|
||||
use crate::ast::CssStmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct CssTree {
|
||||
// None is tombstone
|
||||
stmts: Vec<RefCell<Option<CssStmt>>>,
|
||||
pub parent_to_child: BTreeMap<CssTreeIdx, Vec<CssTreeIdx>>,
|
||||
pub child_to_parent: BTreeMap<CssTreeIdx, CssTreeIdx>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
pub(super) struct CssTreeIdx(usize);
|
||||
|
||||
impl CssTree {
|
||||
pub const ROOT: CssTreeIdx = CssTreeIdx(0);
|
||||
|
||||
pub fn new() -> Self {
|
||||
let mut tree = Self {
|
||||
stmts: Vec::new(),
|
||||
parent_to_child: BTreeMap::new(),
|
||||
child_to_parent: BTreeMap::new(),
|
||||
};
|
||||
|
||||
tree.stmts.push(RefCell::new(None));
|
||||
|
||||
tree
|
||||
}
|
||||
|
||||
pub fn get(&self, idx: CssTreeIdx) -> Ref<Option<CssStmt>> {
|
||||
self.stmts[idx.0].borrow()
|
||||
}
|
||||
|
||||
pub fn get_mut(&self, idx: CssTreeIdx) -> RefMut<Option<CssStmt>> {
|
||||
self.stmts[idx.0].borrow_mut()
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<CssStmt> {
|
||||
let mut idx = 1;
|
||||
|
||||
while idx < self.stmts.len() - 1 {
|
||||
if self.stmts[idx].borrow().is_none() || !self.has_children(CssTreeIdx(idx)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.apply_children(CssTreeIdx(idx));
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
self.stmts
|
||||
.into_iter()
|
||||
.filter_map(RefCell::into_inner)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_children(&self, parent: CssTreeIdx) {
|
||||
for &child in &self.parent_to_child[&parent] {
|
||||
if self.has_children(child) {
|
||||
self.apply_children(child);
|
||||
}
|
||||
|
||||
match self.stmts[child.0].borrow_mut().take() {
|
||||
Some(child) => self.add_child_to_parent(child, parent),
|
||||
None => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn has_children(&self, parent: CssTreeIdx) -> bool {
|
||||
self.parent_to_child.contains_key(&parent)
|
||||
}
|
||||
|
||||
fn add_child_to_parent(&self, child: CssStmt, parent_idx: CssTreeIdx) {
|
||||
let mut parent = self.stmts[parent_idx.0].borrow_mut().take();
|
||||
match &mut parent {
|
||||
Some(CssStmt::RuleSet { body, .. }) => body.push(child),
|
||||
Some(CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..)) | None => {
|
||||
unreachable!()
|
||||
}
|
||||
Some(CssStmt::Media(media, ..)) => {
|
||||
media.body.push(child);
|
||||
}
|
||||
Some(CssStmt::UnknownAtRule(at_rule, ..)) => {
|
||||
at_rule.body.push(child);
|
||||
}
|
||||
Some(CssStmt::Supports(supports, ..)) => {
|
||||
supports.body.push(child);
|
||||
}
|
||||
Some(CssStmt::KeyframesRuleSet(keyframes)) => {
|
||||
keyframes.body.push(child);
|
||||
}
|
||||
}
|
||||
self.stmts[parent_idx.0]
|
||||
.borrow_mut()
|
||||
.replace(parent.unwrap());
|
||||
}
|
||||
|
||||
pub fn add_child(&mut self, child: CssStmt, parent_idx: CssTreeIdx) -> CssTreeIdx {
|
||||
let child_idx = self.add_stmt_inner(child);
|
||||
self.parent_to_child
|
||||
.entry(parent_idx)
|
||||
.or_default()
|
||||
.push(child_idx);
|
||||
self.child_to_parent.insert(child_idx, parent_idx);
|
||||
child_idx
|
||||
}
|
||||
|
||||
pub fn link_child_to_parent(&mut self, child_idx: CssTreeIdx, parent_idx: CssTreeIdx) {
|
||||
self.parent_to_child
|
||||
.entry(parent_idx)
|
||||
.or_default()
|
||||
.push(child_idx);
|
||||
self.child_to_parent.insert(child_idx, parent_idx);
|
||||
}
|
||||
|
||||
pub fn has_following_sibling(&self, child: CssTreeIdx) -> bool {
|
||||
if child == Self::ROOT {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parent_idx = self.child_to_parent.get(&child).unwrap();
|
||||
|
||||
let parent_children = self.parent_to_child.get(parent_idx).unwrap();
|
||||
|
||||
let child_pos = parent_children
|
||||
.iter()
|
||||
.position(|child_idx| *child_idx == child)
|
||||
.unwrap();
|
||||
|
||||
// todo: parent_children[child_pos + 1..] !is_invisible
|
||||
child_pos + 1 < parent_children.len()
|
||||
}
|
||||
|
||||
pub fn add_stmt(&mut self, child: CssStmt, parent: Option<CssTreeIdx>) -> CssTreeIdx {
|
||||
match parent {
|
||||
Some(parent) => self.add_child(child, parent),
|
||||
None => self.add_child(child, Self::ROOT),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_stmt_inner(&mut self, stmt: CssStmt) -> CssTreeIdx {
|
||||
let idx = CssTreeIdx(self.stmts.len());
|
||||
self.stmts.push(RefCell::new(Some(stmt)));
|
||||
|
||||
idx
|
||||
}
|
||||
}
|
281
src/evaluate/env.rs
Normal file
281
src/evaluate/env.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
ast::{AstForwardRule, Mixin},
|
||||
builtin::modules::{ForwardedModule, Module, Modules},
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
selector::ExtensionStore,
|
||||
value::{SassFunction, Value},
|
||||
};
|
||||
use std::{cell::RefCell, collections::BTreeMap, sync::Arc};
|
||||
|
||||
use super::{scope::Scopes, visitor::CallableContentBlock};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Environment {
|
||||
pub scopes: Scopes,
|
||||
pub modules: Arc<RefCell<Modules>>,
|
||||
pub global_modules: Vec<Arc<RefCell<Module>>>,
|
||||
pub content: Option<Arc<CallableContentBlock>>,
|
||||
pub forwarded_modules: Arc<RefCell<Vec<Arc<RefCell<Module>>>>>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scopes: Scopes::new(),
|
||||
modules: Arc::new(RefCell::new(Modules::new())),
|
||||
global_modules: Vec::new(),
|
||||
content: None,
|
||||
forwarded_modules: Arc::new(RefCell::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_closure(&self) -> Self {
|
||||
Self {
|
||||
scopes: self.scopes.new_closure(),
|
||||
modules: Arc::clone(&self.modules),
|
||||
global_modules: self.global_modules.iter().map(Arc::clone).collect(),
|
||||
content: self.content.as_ref().map(Arc::clone),
|
||||
forwarded_modules: Arc::clone(&self.forwarded_modules),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward_module(&mut self, module: Arc<RefCell<Module>>, rule: AstForwardRule) {
|
||||
let view = ForwardedModule::if_necessary(module, rule);
|
||||
(*self.forwarded_modules).borrow_mut().push(view);
|
||||
|
||||
// todo: assertnoconflicts
|
||||
}
|
||||
|
||||
pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) {
|
||||
self.scopes.insert_mixin(name, mixin);
|
||||
}
|
||||
|
||||
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
||||
self.scopes.mixin_exists(name)
|
||||
}
|
||||
|
||||
pub fn get_mixin(
|
||||
&self,
|
||||
name: Spanned<Identifier>,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
) -> SassResult<Mixin> {
|
||||
if let Some(namespace) = namespace {
|
||||
let modules = (*self.modules).borrow();
|
||||
let module = modules.get(namespace.node, namespace.span)?;
|
||||
return (*module).borrow().get_mixin(name);
|
||||
}
|
||||
|
||||
match self.scopes.get_mixin(name) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
if let Some(v) = self.get_mixin_from_global_modules(name.node) {
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_fn(&mut self, func: SassFunction) {
|
||||
self.scopes.insert_fn(func);
|
||||
}
|
||||
|
||||
pub fn fn_exists(&self, name: Identifier) -> bool {
|
||||
self.scopes.fn_exists(name)
|
||||
}
|
||||
|
||||
pub fn get_fn(
|
||||
&self,
|
||||
name: Identifier,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
) -> SassResult<Option<SassFunction>> {
|
||||
if let Some(namespace) = namespace {
|
||||
let modules = (*self.modules).borrow();
|
||||
let module = modules.get(namespace.node, namespace.span)?;
|
||||
return Ok((*module).borrow().get_fn(name));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.scopes
|
||||
.get_fn(name)
|
||||
.or_else(|| self.get_function_from_global_modules(name)))
|
||||
}
|
||||
|
||||
pub fn var_exists(
|
||||
&self,
|
||||
name: Identifier,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
) -> SassResult<bool> {
|
||||
if let Some(namespace) = namespace {
|
||||
let modules = (*self.modules).borrow();
|
||||
let module = modules.get(namespace.node, namespace.span)?;
|
||||
return Ok((*module).borrow().var_exists(name));
|
||||
}
|
||||
|
||||
Ok(self.scopes.var_exists(name))
|
||||
}
|
||||
|
||||
pub fn get_var(
|
||||
&self,
|
||||
name: Spanned<Identifier>,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
) -> SassResult<Value> {
|
||||
if let Some(namespace) = namespace {
|
||||
let modules = (*self.modules).borrow();
|
||||
let module = modules.get(namespace.node, namespace.span)?;
|
||||
return (*module).borrow().get_var(name);
|
||||
}
|
||||
|
||||
match self.scopes.get_var(name) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
if let Some(v) = self.get_variable_from_global_modules(name.node) {
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_var(
|
||||
&mut self,
|
||||
name: Spanned<Identifier>,
|
||||
namespace: Option<Spanned<Identifier>>,
|
||||
value: Value,
|
||||
is_global: bool,
|
||||
in_semi_global_scope: bool,
|
||||
) -> SassResult<()> {
|
||||
if let Some(namespace) = namespace {
|
||||
let mut modules = (*self.modules).borrow_mut();
|
||||
let module = modules.get_mut(namespace.node, namespace.span)?;
|
||||
(*module).borrow_mut().update_var(name, value)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if is_global || self.at_root() {
|
||||
// // Don't set the index if there's already a variable with the given name,
|
||||
// // since local accesses should still return the local variable.
|
||||
// _variableIndices.putIfAbsent(name, () {
|
||||
// _lastVariableName = name;
|
||||
// _lastVariableIndex = 0;
|
||||
// return 0;
|
||||
// });
|
||||
|
||||
// // If this module doesn't already contain a variable named [name], try
|
||||
// // setting it in a global module.
|
||||
// if (!_variables.first.containsKey(name)) {
|
||||
// var moduleWithName = _fromOneModule(name, "variable",
|
||||
// (module) => module.variables.containsKey(name) ? module : null);
|
||||
// if (moduleWithName != null) {
|
||||
// moduleWithName.setVariable(name, value, nodeWithSpan);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
self.scopes.insert_var(0, name.node, value);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut index = self
|
||||
.scopes
|
||||
.find_var(name.node)
|
||||
.unwrap_or(self.scopes.len() - 1);
|
||||
|
||||
if !in_semi_global_scope && index == 0 {
|
||||
index = self.scopes.len() - 1;
|
||||
}
|
||||
|
||||
self.scopes.insert_var(index, name.node, value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn at_root(&self) -> bool {
|
||||
self.scopes.len() == 1
|
||||
}
|
||||
|
||||
pub fn scopes_mut(&mut self) -> &mut Scopes {
|
||||
&mut self.scopes
|
||||
}
|
||||
|
||||
pub fn global_vars(&self) -> Arc<RefCell<BTreeMap<Identifier, Value>>> {
|
||||
self.scopes.global_variables()
|
||||
}
|
||||
|
||||
pub fn global_mixins(&self) -> Arc<RefCell<BTreeMap<Identifier, Mixin>>> {
|
||||
self.scopes.global_mixins()
|
||||
}
|
||||
|
||||
pub fn global_functions(&self) -> Arc<RefCell<BTreeMap<Identifier, SassFunction>>> {
|
||||
self.scopes.global_functions()
|
||||
}
|
||||
|
||||
fn get_variable_from_global_modules(&self, name: Identifier) -> Option<Value> {
|
||||
for module in &self.global_modules {
|
||||
if (**module).borrow().var_exists(name) {
|
||||
return (**module).borrow().get_var_no_err(name);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_function_from_global_modules(&self, name: Identifier) -> Option<SassFunction> {
|
||||
for module in &self.global_modules {
|
||||
if (**module).borrow().fn_exists(name) {
|
||||
return (**module).borrow().get_fn(name);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_mixin_from_global_modules(&self, name: Identifier) -> Option<Mixin> {
|
||||
for module in &self.global_modules {
|
||||
if (**module).borrow().mixin_exists(name) {
|
||||
return (**module).borrow().get_mixin_no_err(name);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add_module(
|
||||
&mut self,
|
||||
namespace: Option<Identifier>,
|
||||
module: Arc<RefCell<Module>>,
|
||||
span: Span,
|
||||
) -> SassResult<()> {
|
||||
match namespace {
|
||||
Some(namespace) => {
|
||||
(*self.modules)
|
||||
.borrow_mut()
|
||||
.insert(namespace, module, span)?;
|
||||
}
|
||||
None => {
|
||||
for name in (*self.scopes.global_variables()).borrow().keys() {
|
||||
if (*module).borrow().var_exists(*name) {
|
||||
return Err((
|
||||
format!("This module and the new module both define a variable named \"{name}\".")
|
||||
, span).into());
|
||||
}
|
||||
}
|
||||
|
||||
self.global_modules.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_module(self, extension_store: ExtensionStore) -> Arc<RefCell<Module>> {
|
||||
debug_assert!(self.at_root());
|
||||
|
||||
Arc::new(RefCell::new(Module::new_env(self, extension_store)))
|
||||
}
|
||||
}
|
9
src/evaluate/mod.rs
Normal file
9
src/evaluate/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub(crate) use bin_op::{cmp, div};
|
||||
pub(crate) use env::Environment;
|
||||
pub(crate) use visitor::*;
|
||||
|
||||
mod bin_op;
|
||||
mod css_tree;
|
||||
mod env;
|
||||
mod scope;
|
||||
mod visitor;
|
213
src/evaluate/scope.rs
Normal file
213
src/evaluate/scope.rs
Normal file
@ -0,0 +1,213 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::BTreeMap,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
ast::Mixin,
|
||||
builtin::GLOBAL_FUNCTIONS,
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
value::{SassFunction, Value},
|
||||
};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct Scopes {
|
||||
variables: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, Value>>>>>>,
|
||||
mixins: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, Mixin>>>>>>,
|
||||
functions: Arc<RefCell<Vec<Arc<RefCell<BTreeMap<Identifier, SassFunction>>>>>>,
|
||||
len: Arc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Scopes {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
|
||||
mixins: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
|
||||
functions: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])),
|
||||
len: Arc::new(Cell::new(1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_closure(&self) -> Self {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
Self {
|
||||
variables: Arc::new(RefCell::new(
|
||||
(*self.variables).borrow().iter().map(Arc::clone).collect(),
|
||||
)),
|
||||
mixins: Arc::new(RefCell::new(
|
||||
(*self.mixins).borrow().iter().map(Arc::clone).collect(),
|
||||
)),
|
||||
functions: Arc::new(RefCell::new(
|
||||
(*self.functions).borrow().iter().map(Arc::clone).collect(),
|
||||
)),
|
||||
len: Arc::new(Cell::new(self.len())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_variables(&self) -> Arc<RefCell<BTreeMap<Identifier, Value>>> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
Arc::clone(&(*self.variables).borrow()[0])
|
||||
}
|
||||
|
||||
pub fn global_functions(&self) -> Arc<RefCell<BTreeMap<Identifier, SassFunction>>> {
|
||||
Arc::clone(&(*self.functions).borrow()[0])
|
||||
}
|
||||
|
||||
pub fn global_mixins(&self) -> Arc<RefCell<BTreeMap<Identifier, Mixin>>> {
|
||||
Arc::clone(&(*self.mixins).borrow()[0])
|
||||
}
|
||||
|
||||
pub fn find_var(&self, name: Identifier) -> Option<usize> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() {
|
||||
if (**scope).borrow().contains_key(&name) {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
(*self.len).get()
|
||||
}
|
||||
|
||||
pub fn enter_new_scope(&mut self) {
|
||||
let len = self.len();
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
(*self.len).set(len + 1);
|
||||
(*self.variables)
|
||||
.borrow_mut()
|
||||
.push(Arc::new(RefCell::new(BTreeMap::new())));
|
||||
(*self.mixins)
|
||||
.borrow_mut()
|
||||
.push(Arc::new(RefCell::new(BTreeMap::new())));
|
||||
(*self.functions)
|
||||
.borrow_mut()
|
||||
.push(Arc::new(RefCell::new(BTreeMap::new())));
|
||||
}
|
||||
|
||||
pub fn exit_scope(&mut self) {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
let len = self.len();
|
||||
(*self.len).set(len - 1);
|
||||
(*self.variables).borrow_mut().pop();
|
||||
(*self.mixins).borrow_mut().pop();
|
||||
(*self.functions).borrow_mut().pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables
|
||||
impl Scopes {
|
||||
pub fn insert_var(&mut self, idx: usize, name: Identifier, v: Value) -> Option<Value> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
(*(*self.variables).borrow_mut()[idx])
|
||||
.borrow_mut()
|
||||
.insert(name, v)
|
||||
}
|
||||
|
||||
/// Always insert this variable into the innermost scope
|
||||
///
|
||||
/// Used, for example, for variables from `@each` and `@for`
|
||||
pub fn insert_var_last(&mut self, name: Identifier, v: Value) -> Option<Value> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
(*(*self.variables).borrow_mut()[self.len() - 1])
|
||||
.borrow_mut()
|
||||
.insert(name, v)
|
||||
}
|
||||
|
||||
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<Value> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.variables).borrow().iter().rev() {
|
||||
match (**scope).borrow().get(&name.node) {
|
||||
Some(var) => return Ok(var.clone()),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Err(("Undefined variable.", name.span).into())
|
||||
}
|
||||
|
||||
pub fn var_exists(&self, name: Identifier) -> bool {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.variables).borrow().iter() {
|
||||
if (**scope).borrow().contains_key(&name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixins
|
||||
impl Scopes {
|
||||
pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
(*(*self.mixins).borrow_mut().last_mut().unwrap())
|
||||
.borrow_mut()
|
||||
.insert(name, mixin);
|
||||
}
|
||||
|
||||
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.mixins).borrow().iter().rev() {
|
||||
match (**scope).borrow().get(&name.node) {
|
||||
Some(mixin) => return Ok(mixin.clone()),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Err(("Undefined mixin.", name.span).into())
|
||||
}
|
||||
|
||||
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.mixins).borrow().iter() {
|
||||
if (**scope).borrow().contains_key(&name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions
|
||||
impl Scopes {
|
||||
pub fn insert_fn(&mut self, func: SassFunction) {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
(*(*self.functions).borrow_mut().last_mut().unwrap())
|
||||
.borrow_mut()
|
||||
.insert(func.name(), func);
|
||||
}
|
||||
|
||||
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.functions).borrow().iter().rev() {
|
||||
let func = (**scope).borrow().get(&name).cloned();
|
||||
|
||||
if func.is_some() {
|
||||
return func;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn fn_exists(&self, name: Identifier) -> bool {
|
||||
debug_assert_eq!(self.len(), (*self.variables).borrow().len());
|
||||
for scope in (*self.functions).borrow().iter() {
|
||||
if (**scope).borrow().contains_key(&name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
GLOBAL_FUNCTIONS.contains_key(name.as_str())
|
||||
}
|
||||
}
|
2878
src/evaluate/visitor.rs
Normal file
2878
src/evaluate/visitor.rs
Normal file
File diff suppressed because it is too large
Load Diff
12
src/fs.rs
12
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<Vec<u8>>;
|
||||
fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// Use [`std::fs`] to read any files from disk.
|
||||
@ -36,7 +38,7 @@ impl Fs for StdFs {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(&self, path: &Path) -> Result<Vec<u8>> {
|
||||
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
std::fs::read(path)
|
||||
}
|
||||
}
|
||||
@ -61,7 +63,7 @@ impl Fs for NullFs {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(&self, _path: &Path) -> Result<Vec<u8>> {
|
||||
fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(Error::new(
|
||||
ErrorKind::NotFound,
|
||||
"NullFs, there is no file system",
|
||||
|
@ -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)) }
|
||||
}
|
||||
|
83
src/lexer.rs
83
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 next_char_is(&self, c: char) -> bool {
|
||||
matches!(self.peek(), Some(Token { kind, .. }) if kind == c)
|
||||
}
|
||||
|
||||
pub fn span_from(&mut self, start: usize) -> Span {
|
||||
let start = match self.buf.get(start) {
|
||||
Some(start) => start.pos,
|
||||
None => return self.current_span(),
|
||||
};
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
let end = self.current_span();
|
||||
self.cursor += 1;
|
||||
|
||||
start.merge(end)
|
||||
}
|
||||
|
||||
pub fn prev_span(&self) -> Span {
|
||||
self.buf
|
||||
.get(self.cursor.saturating_sub(1))
|
||||
.copied()
|
||||
.unwrap_or_else(|| self.buf.last().copied().unwrap())
|
||||
.pos
|
||||
}
|
||||
|
||||
pub fn current_span(&self) -> Span {
|
||||
self.buf
|
||||
.get(self.cursor)
|
||||
.copied()
|
||||
.unwrap_or_else(|| self.buf.last().copied().unwrap())
|
||||
.pos
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<Token> {
|
||||
self.buf.get(self.peek_cursor()).copied()
|
||||
}
|
||||
|
||||
pub fn reset_cursor(&mut self) {
|
||||
self.amt_peeked = 0;
|
||||
}
|
||||
|
||||
pub fn peek_next(&mut self) -> Option<Token> {
|
||||
self.amt_peeked += 1;
|
||||
|
||||
self.peek()
|
||||
self.buf.get(self.cursor).copied()
|
||||
}
|
||||
|
||||
/// Peeks the previous token without modifying the peek cursor
|
||||
pub fn peek_previous(&mut self) -> Option<Token> {
|
||||
self.buf.get(self.peek_cursor().checked_sub(1)?).copied()
|
||||
}
|
||||
|
||||
pub fn peek_forward(&mut self, n: usize) -> Option<Token> {
|
||||
self.amt_peeked += n;
|
||||
|
||||
self.peek()
|
||||
self.buf.get(self.cursor.checked_sub(1)?).copied()
|
||||
}
|
||||
|
||||
/// Peeks `n` from current peeked position without modifying cursor
|
||||
pub fn peek_n(&self, n: usize) -> Option<Token> {
|
||||
self.buf.get(self.peek_cursor() + n).copied()
|
||||
self.buf.get(self.cursor + n).copied()
|
||||
}
|
||||
|
||||
pub fn peek_backward(&mut self, n: usize) -> Option<Token> {
|
||||
self.amt_peeked = self.amt_peeked.checked_sub(n)?;
|
||||
|
||||
self.peek()
|
||||
/// Peeks `n` behind current peeked position without modifying cursor
|
||||
pub fn peek_n_backwards(&self, n: usize) -> Option<Token> {
|
||||
self.buf.get(self.cursor.checked_sub(n)?).copied()
|
||||
}
|
||||
|
||||
/// Set cursor to position and reset peek
|
||||
pub fn set_cursor(&mut self, cursor: usize) {
|
||||
self.cursor = cursor;
|
||||
self.amt_peeked = 0;
|
||||
}
|
||||
|
||||
pub fn cursor(&self) -> usize {
|
||||
@ -70,7 +87,6 @@ impl<'a> Iterator for Lexer<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.buf.get(self.cursor).copied().map(|tok| {
|
||||
self.cursor += 1;
|
||||
self.amt_peeked = self.amt_peeked.saturating_sub(1);
|
||||
tok
|
||||
})
|
||||
}
|
||||
@ -122,15 +138,6 @@ impl<'a> Lexer<'a> {
|
||||
Lexer {
|
||||
buf: Cow::Owned(buf),
|
||||
cursor: 0,
|
||||
amt_peeked: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_ref(buf: &'a [Token]) -> Lexer<'a> {
|
||||
Lexer {
|
||||
buf: Cow::Borrowed(buf),
|
||||
cursor: 0,
|
||||
amt_peeked: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
286
src/lib.rs
286
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<grass::Error>> {
|
||||
let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
|
||||
assert_eq!(sass, "a b {\n color: a b;\n}\n");
|
||||
let css = grass::from_string(
|
||||
"a { b { color: &; } }".to_owned(),
|
||||
&grass::Options::default()
|
||||
)?;
|
||||
assert_eq!(css, "a b {\n color: a b;\n}\n");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
@ -23,7 +20,7 @@ grass input.scss
|
||||
```
|
||||
*/
|
||||
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||
#![warn(clippy::all, clippy::cargo)]
|
||||
#![deny(missing_debug_implementations)]
|
||||
#![allow(
|
||||
clippy::use_self,
|
||||
@ -57,200 +54,54 @@ grass input.scss
|
||||
clippy::items_after_statements,
|
||||
// this is only available on nightly
|
||||
clippy::unnested_or_patterns,
|
||||
clippy::uninlined_format_args,
|
||||
|
||||
// todo:
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_lossless,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::float_cmp,
|
||||
clippy::wildcard_imports,
|
||||
clippy::comparison_chain,
|
||||
clippy::bool_to_int_with_if,
|
||||
)]
|
||||
#![cfg_attr(feature = "profiling", inline(never))]
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use parse::{CssParser, SassParser, StylesheetParser};
|
||||
use serializer::Serializer;
|
||||
#[cfg(feature = "wasm-exports")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub(crate) use beef::lean::Cow;
|
||||
|
||||
use codemap::CodeMap;
|
||||
|
||||
pub use crate::error::{
|
||||
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
|
||||
};
|
||||
pub use crate::fs::{Fs, NullFs, StdFs};
|
||||
pub(crate) use crate::token::Token;
|
||||
use crate::{
|
||||
builtin::modules::{ModuleConfig, Modules},
|
||||
lexer::Lexer,
|
||||
output::{AtRuleContext, Css},
|
||||
parse::{
|
||||
common::{ContextFlags, NeverEmptyVec},
|
||||
Parser,
|
||||
},
|
||||
scope::{Scope, Scopes},
|
||||
selector::{ExtendedSelector, Extender, SelectorList},
|
||||
};
|
||||
pub use crate::options::{InputSyntax, Options, OutputStyle};
|
||||
pub(crate) use crate::{context_flags::ContextFlags, token::Token};
|
||||
use crate::{evaluate::Visitor, lexer::Lexer, parse::ScssParser};
|
||||
|
||||
mod args;
|
||||
mod atrule;
|
||||
mod ast;
|
||||
mod builtin;
|
||||
mod color;
|
||||
mod common;
|
||||
mod context_flags;
|
||||
mod error;
|
||||
mod evaluate;
|
||||
mod fs;
|
||||
mod interner;
|
||||
mod lexer;
|
||||
mod output;
|
||||
mod options;
|
||||
mod parse;
|
||||
mod scope;
|
||||
mod selector;
|
||||
mod style;
|
||||
mod serializer;
|
||||
mod token;
|
||||
mod unit;
|
||||
mod utils;
|
||||
mod value;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum OutputStyle {
|
||||
/// The default style, this mode writes each
|
||||
/// selector and declaration on its own line.
|
||||
Expanded,
|
||||
/// Ideal for release builds, this mode removes
|
||||
/// as many extra characters as possible and
|
||||
/// writes the entire stylesheet on a single line.
|
||||
Compressed,
|
||||
}
|
||||
|
||||
/// Configuration for Sass compilation
|
||||
///
|
||||
/// The simplest usage is `grass::Options::default()`;
|
||||
/// however, a builder pattern is also exposed to offer
|
||||
/// more control.
|
||||
#[derive(Debug)]
|
||||
pub struct Options<'a> {
|
||||
fs: &'a dyn Fs,
|
||||
style: OutputStyle,
|
||||
load_paths: Vec<&'a Path>,
|
||||
allows_charset: bool,
|
||||
unicode_error_messages: bool,
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
impl Default for Options<'_> {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fs: &StdFs,
|
||||
style: OutputStyle::Expanded,
|
||||
load_paths: Vec::new(),
|
||||
allows_charset: true,
|
||||
unicode_error_messages: true,
|
||||
quiet: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Options<'a> {
|
||||
/// This option allows you to control the file system that Sass will see.
|
||||
///
|
||||
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
|
||||
/// allowing direct, unfettered access to the local file system.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
|
||||
self.fs = fs;
|
||||
self
|
||||
}
|
||||
|
||||
/// `grass` currently offers 2 different output styles
|
||||
///
|
||||
/// - `OutputStyle::Expanded` writes each selector and declaration on its own line.
|
||||
/// - `OutputStyle::Compressed` removes as many extra characters as possible
|
||||
/// and writes the entire stylesheet on a single line.
|
||||
///
|
||||
/// By default, output is expanded.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn style(mut self, style: OutputStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass not to emit any warnings
|
||||
/// when compiling. By default, Sass emits warnings
|
||||
/// when deprecated features are used or when the
|
||||
/// `@warn` rule is encountered. It also silences the
|
||||
/// `@debug` rule.
|
||||
///
|
||||
/// By default, this value is `false` and warnings are emitted.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn quiet(mut self, quiet: bool) -> Self {
|
||||
self.quiet = quiet;
|
||||
self
|
||||
}
|
||||
|
||||
/// All Sass implementations allow users to provide
|
||||
/// load paths: paths on the filesystem that Sass
|
||||
/// will look in when locating modules. For example,
|
||||
/// if you pass `node_modules/susy/sass` as a load path,
|
||||
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
|
||||
///
|
||||
/// Imports will always be resolved relative to the current
|
||||
/// file first, though. Load paths will only be used if no
|
||||
/// relative file exists that matches the module's URL. This
|
||||
/// ensures that you can't accidentally mess up your relative
|
||||
/// imports when you add a new library.
|
||||
///
|
||||
/// This method will append a single path to the list.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn load_path(mut self, path: &'a Path) -> Self {
|
||||
self.load_paths.push(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Append multiple loads paths
|
||||
///
|
||||
/// Note that this method does *not* remove existing load paths
|
||||
///
|
||||
/// See [`Options::load_path`](Options::load_path) for more information about load paths
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
|
||||
self.load_paths.extend_from_slice(paths);
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass whether to emit a `@charset`
|
||||
/// declaration or a UTF-8 byte-order mark.
|
||||
///
|
||||
/// By default, Sass will insert either a `@charset`
|
||||
/// declaration (in expanded output mode) or a byte-order
|
||||
/// mark (in compressed output mode) if the stylesheet
|
||||
/// contains any non-ASCII characters.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn allows_charset(mut self, allows_charset: bool) -> Self {
|
||||
self.allows_charset = allows_charset;
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass only to emit ASCII characters as
|
||||
/// part of error messages.
|
||||
///
|
||||
/// By default Sass will emit non-ASCII characters for
|
||||
/// these messages.
|
||||
///
|
||||
/// This flag does not affect the CSS output.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
|
||||
self.unicode_error_messages = unicode_error_messages;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn is_compressed(&self) -> bool {
|
||||
matches!(self.style, OutputStyle::Compressed)
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
|
||||
let (message, span) = err.raw();
|
||||
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
|
||||
@ -260,34 +111,59 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
|
||||
let mut map = CodeMap::new();
|
||||
let file = map.add_file(file_name.to_owned(), input);
|
||||
let empty_span = file.span.subspan(0, 0);
|
||||
let lexer = Lexer::new_from_file(&file);
|
||||
|
||||
let stmts = Parser {
|
||||
toks: &mut Lexer::new_from_file(&file),
|
||||
map: &mut map,
|
||||
path: file_name.as_ref(),
|
||||
scopes: &mut Scopes::new(),
|
||||
global_scope: &mut Scope::new(),
|
||||
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new(
|
||||
empty_span,
|
||||
))),
|
||||
span_before: empty_span,
|
||||
content: &mut Vec::new(),
|
||||
flags: ContextFlags::empty(),
|
||||
at_root: true,
|
||||
at_root_has_selector: false,
|
||||
extender: &mut Extender::new(empty_span),
|
||||
content_scopes: &mut Scopes::new(),
|
||||
options,
|
||||
modules: &mut Modules::default(),
|
||||
module_config: &mut ModuleConfig::default(),
|
||||
let path = Path::new(file_name);
|
||||
|
||||
let input_syntax = options
|
||||
.input_syntax
|
||||
.unwrap_or_else(|| InputSyntax::for_path(path));
|
||||
|
||||
let stylesheet = match input_syntax {
|
||||
InputSyntax::Scss => {
|
||||
ScssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||
}
|
||||
.parse()
|
||||
InputSyntax::Sass => {
|
||||
SassParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||
}
|
||||
InputSyntax::Css => {
|
||||
CssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||
}
|
||||
};
|
||||
|
||||
let stylesheet = match stylesheet {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
|
||||
};
|
||||
|
||||
let mut visitor = Visitor::new(path, options, &mut map, empty_span);
|
||||
match visitor.visit_stylesheet(stylesheet) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
|
||||
}
|
||||
let stmts = visitor.finish();
|
||||
|
||||
let mut serializer = Serializer::new(options, &map, false, empty_span);
|
||||
|
||||
let mut prev_was_group_end = false;
|
||||
let mut prev_requires_semicolon = false;
|
||||
for stmt in stmts {
|
||||
if stmt.is_invisible() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_group_end = stmt.is_group_end();
|
||||
let requires_semicolon = Serializer::requires_semicolon(&stmt);
|
||||
|
||||
serializer
|
||||
.visit_group(stmt, prev_was_group_end, prev_requires_semicolon)
|
||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
|
||||
|
||||
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))
|
||||
prev_was_group_end = is_group_end;
|
||||
prev_requires_semicolon = requires_semicolon;
|
||||
}
|
||||
|
||||
Ok(serializer.finish(prev_requires_semicolon))
|
||||
}
|
||||
|
||||
/// Compile CSS from a path
|
||||
@ -300,8 +176,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
||||
|
||||
#[inline]
|
||||
pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
from_string_with_file_name(
|
||||
String::from_utf8(options.fs.read(Path::new(p))?)?,
|
||||
@ -319,8 +195,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
||||
|
||||
#[inline]
|
||||
pub fn from_string(input: String, options: &Options) -> Result<String> {
|
||||
from_string_with_file_name(input, "stdin", options)
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ arg_enum! {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
fn main() -> std::io::Result<()> {
|
||||
let matches = App::new("grass")
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
@ -144,6 +143,12 @@ fn main() -> std::io::Result<()> {
|
||||
.hidden(true)
|
||||
.help("Whether to use terminal colors for messages.")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("VERBOSE")
|
||||
.long("verbose")
|
||||
.hidden(true)
|
||||
.help("Print all deprecation warnings even when they're repetitive.")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("NO_UNICODE")
|
||||
.long("no-unicode")
|
||||
|
193
src/options.rs
Normal file
193
src/options.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Fs, StdFs};
|
||||
|
||||
/// Configuration for Sass compilation
|
||||
///
|
||||
/// The simplest usage is `grass::Options::default()`;
|
||||
/// however, a builder pattern is also exposed to offer
|
||||
/// more control.
|
||||
#[derive(Debug)]
|
||||
pub struct Options<'a> {
|
||||
pub(crate) fs: &'a dyn Fs,
|
||||
pub(crate) style: OutputStyle,
|
||||
pub(crate) load_paths: Vec<&'a Path>,
|
||||
pub(crate) allows_charset: bool,
|
||||
pub(crate) unicode_error_messages: bool,
|
||||
pub(crate) quiet: bool,
|
||||
pub(crate) input_syntax: Option<InputSyntax>,
|
||||
}
|
||||
|
||||
impl Default for Options<'_> {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fs: &StdFs,
|
||||
style: OutputStyle::Expanded,
|
||||
load_paths: Vec::new(),
|
||||
allows_charset: true,
|
||||
unicode_error_messages: true,
|
||||
quiet: false,
|
||||
input_syntax: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Options<'a> {
|
||||
/// This option allows you to control the file system that Sass will see.
|
||||
///
|
||||
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
|
||||
/// allowing direct, unfettered access to the local file system.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
|
||||
self.fs = fs;
|
||||
self
|
||||
}
|
||||
|
||||
/// `grass` currently offers 2 different output styles
|
||||
///
|
||||
/// - [`OutputStyle::Expanded`] writes each selector and declaration on its own line.
|
||||
/// - [`OutputStyle::Compressed`] removes as many extra characters as possible
|
||||
/// and writes the entire stylesheet on a single line.
|
||||
///
|
||||
/// By default, output is expanded.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn style(mut self, style: OutputStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass not to emit any warnings
|
||||
/// when compiling. By default, Sass emits warnings
|
||||
/// when deprecated features are used or when the
|
||||
/// `@warn` rule is encountered. It also silences the
|
||||
/// `@debug` rule.
|
||||
///
|
||||
/// By default, this value is `false` and warnings are emitted.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn quiet(mut self, quiet: bool) -> Self {
|
||||
self.quiet = quiet;
|
||||
self
|
||||
}
|
||||
|
||||
/// All Sass implementations allow users to provide
|
||||
/// load paths: paths on the filesystem that Sass
|
||||
/// will look in when locating modules. For example,
|
||||
/// if you pass `node_modules/susy/sass` as a load path,
|
||||
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
|
||||
///
|
||||
/// Imports will always be resolved relative to the current
|
||||
/// file first, though. Load paths will only be used if no
|
||||
/// relative file exists that matches the module's URL. This
|
||||
/// ensures that you can't accidentally mess up your relative
|
||||
/// imports when you add a new library.
|
||||
///
|
||||
/// This method will append a single path to the list.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn load_path(mut self, path: &'a Path) -> Self {
|
||||
self.load_paths.push(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Append multiple loads paths
|
||||
///
|
||||
/// Note that this method does *not* remove existing load paths
|
||||
///
|
||||
/// See [`Options::load_path`](Options::load_path) for more information about
|
||||
/// load paths
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
|
||||
self.load_paths.extend_from_slice(paths);
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass whether to emit a `@charset`
|
||||
/// declaration or a UTF-8 byte-order mark.
|
||||
///
|
||||
/// By default, Sass will insert either a `@charset`
|
||||
/// declaration (in expanded output mode) or a byte-order
|
||||
/// mark (in compressed output mode) if the stylesheet
|
||||
/// contains any non-ASCII characters.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn allows_charset(mut self, allows_charset: bool) -> Self {
|
||||
self.allows_charset = allows_charset;
|
||||
self
|
||||
}
|
||||
|
||||
/// This flag tells Sass only to emit ASCII characters as
|
||||
/// part of error messages.
|
||||
///
|
||||
/// By default Sass will emit non-ASCII characters for
|
||||
/// these messages.
|
||||
///
|
||||
/// This flag does not affect the CSS output.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
|
||||
self.unicode_error_messages = unicode_error_messages;
|
||||
self
|
||||
}
|
||||
|
||||
/// This option forces Sass to parse input using the given syntax.
|
||||
///
|
||||
/// By default, Sass will attempt to read the file extension to determine
|
||||
/// the syntax. If this is not possible, it will default to [`InputSyntax::Scss`]
|
||||
///
|
||||
/// This flag only affects the first file loaded. Files that are loaded using
|
||||
/// `@import`, `@use`, or `@forward` will always have their syntax inferred.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn input_syntax(mut self, syntax: InputSyntax) -> Self {
|
||||
self.input_syntax = Some(syntax);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn is_compressed(&self) -> bool {
|
||||
matches!(self.style, OutputStyle::Compressed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful when parsing Sass from sources other than the file system
|
||||
///
|
||||
/// See [`Options::input_syntax`] for additional information
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum InputSyntax {
|
||||
/// The CSS-superset SCSS syntax.
|
||||
Scss,
|
||||
|
||||
/// The whitespace-sensitive indented syntax.
|
||||
Sass,
|
||||
|
||||
/// The plain CSS syntax, which disallows special Sass features.
|
||||
Css,
|
||||
}
|
||||
|
||||
impl InputSyntax {
|
||||
pub(crate) fn for_path(path: &Path) -> Self {
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("css") => Self::Css,
|
||||
Some("sass") => Self::Sass,
|
||||
_ => Self::Scss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum OutputStyle {
|
||||
/// This mode writes each selector and declaration on its own line.
|
||||
///
|
||||
/// This is the default output.
|
||||
Expanded,
|
||||
|
||||
/// Ideal for release builds, this mode removes as many extra characters as
|
||||
/// possible and writes the entire stylesheet on a single line.
|
||||
Compressed,
|
||||
}
|
782
src/output.rs
782
src/output.rs
@ -1,782 +0,0 @@
|
||||
//! # Convert from SCSS AST to CSS
|
||||
use std::{io::Write, mem};
|
||||
|
||||
use codemap::CodeMap;
|
||||
|
||||
use crate::{
|
||||
atrule::{
|
||||
keyframes::{Keyframes, KeyframesRuleSet, KeyframesSelector},
|
||||
media::MediaRule,
|
||||
SupportsRule, UnknownAtRule,
|
||||
},
|
||||
error::SassResult,
|
||||
parse::Stmt,
|
||||
selector::{ComplexSelector, ComplexSelectorComponent, Selector},
|
||||
style::Style,
|
||||
OutputStyle,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ToplevelUnknownAtRule {
|
||||
name: String,
|
||||
params: String,
|
||||
body: Vec<Stmt>,
|
||||
has_body: bool,
|
||||
is_group_end: bool,
|
||||
inside_rule: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BlockEntryUnknownAtRule {
|
||||
name: String,
|
||||
params: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Toplevel {
|
||||
RuleSet {
|
||||
selector: Selector,
|
||||
body: Vec<BlockEntry>,
|
||||
is_group_end: bool,
|
||||
},
|
||||
MultilineComment(String),
|
||||
UnknownAtRule(Box<ToplevelUnknownAtRule>),
|
||||
Keyframes(Box<Keyframes>),
|
||||
KeyframesRuleSet(Vec<KeyframesSelector>, Vec<BlockEntry>),
|
||||
Media {
|
||||
query: String,
|
||||
body: Vec<Stmt>,
|
||||
inside_rule: bool,
|
||||
is_group_end: bool,
|
||||
},
|
||||
Supports {
|
||||
params: String,
|
||||
body: Vec<Stmt>,
|
||||
inside_rule: bool,
|
||||
is_group_end: bool,
|
||||
},
|
||||
// todo: do we actually need a toplevel style variant?
|
||||
Style(Style),
|
||||
Import(String),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Toplevel {
|
||||
pub fn is_invisible(&self) -> bool {
|
||||
match self {
|
||||
Toplevel::RuleSet { selector, body, .. } => selector.is_empty() || body.is_empty(),
|
||||
Toplevel::Media { body, .. } => body.is_empty(),
|
||||
Toplevel::Empty => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_group_end(&self) -> bool {
|
||||
match self {
|
||||
Toplevel::RuleSet { is_group_end, .. } => *is_group_end,
|
||||
Toplevel::UnknownAtRule(t) => t.is_group_end && t.inside_rule,
|
||||
Toplevel::Media {
|
||||
inside_rule,
|
||||
is_group_end,
|
||||
..
|
||||
}
|
||||
| Toplevel::Supports {
|
||||
inside_rule,
|
||||
is_group_end,
|
||||
..
|
||||
} => *inside_rule && *is_group_end,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_group_end(group: &mut [Toplevel]) {
|
||||
match group.last_mut() {
|
||||
Some(Toplevel::RuleSet { is_group_end, .. })
|
||||
| Some(Toplevel::Supports { is_group_end, .. })
|
||||
| Some(Toplevel::Media { is_group_end, .. }) => {
|
||||
*is_group_end = true;
|
||||
}
|
||||
Some(Toplevel::UnknownAtRule(t)) => t.is_group_end = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum BlockEntry {
|
||||
Style(Style),
|
||||
MultilineComment(String),
|
||||
UnknownAtRule(BlockEntryUnknownAtRule),
|
||||
}
|
||||
|
||||
impl BlockEntry {
|
||||
pub fn to_string(&self) -> SassResult<String> {
|
||||
match self {
|
||||
BlockEntry::Style(s) => s.to_string(),
|
||||
BlockEntry::MultilineComment(s) => Ok(format!("/*{}*/", s)),
|
||||
BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule { name, params }) => {
|
||||
Ok(if params.is_empty() {
|
||||
format!("@{};", name)
|
||||
} else {
|
||||
format!("@{} {};", name, params)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Toplevel {
|
||||
const fn new_rule(selector: Selector, is_group_end: bool) -> Self {
|
||||
Toplevel::RuleSet {
|
||||
selector,
|
||||
is_group_end,
|
||||
body: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_keyframes_rule(selector: Vec<KeyframesSelector>) -> Self {
|
||||
Toplevel::KeyframesRuleSet(selector, Vec::new())
|
||||
}
|
||||
|
||||
fn push_style(&mut self, s: Style) {
|
||||
if s.value.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Toplevel::RuleSet { body, .. } | Toplevel::KeyframesRuleSet(_, body) = self {
|
||||
body.push(BlockEntry::Style(s));
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
fn push_comment(&mut self, s: String) {
|
||||
if let Toplevel::RuleSet { body, .. } | Toplevel::KeyframesRuleSet(_, body) = self {
|
||||
body.push(BlockEntry::MultilineComment(s));
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unknown_at_rule(&mut self, at_rule: ToplevelUnknownAtRule) {
|
||||
if let Toplevel::RuleSet { body, .. } = self {
|
||||
body.push(BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule {
|
||||
name: at_rule.name,
|
||||
params: at_rule.params,
|
||||
}));
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Css {
|
||||
blocks: Vec<Toplevel>,
|
||||
at_rule_context: AtRuleContext,
|
||||
allows_charset: bool,
|
||||
plain_imports: Vec<Toplevel>,
|
||||
}
|
||||
|
||||
impl Css {
|
||||
pub const fn new(at_rule_context: AtRuleContext, allows_charset: bool) -> Self {
|
||||
Css {
|
||||
blocks: Vec::new(),
|
||||
at_rule_context,
|
||||
allows_charset,
|
||||
plain_imports: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_stmts(
|
||||
s: Vec<Stmt>,
|
||||
at_rule_context: AtRuleContext,
|
||||
allows_charset: bool,
|
||||
) -> SassResult<Self> {
|
||||
Css::new(at_rule_context, allows_charset).parse_stylesheet(s)
|
||||
}
|
||||
|
||||
fn parse_stmt(&mut self, stmt: Stmt) -> SassResult<Vec<Toplevel>> {
|
||||
Ok(match stmt {
|
||||
Stmt::RuleSet { selector, body } => {
|
||||
if body.is_empty() {
|
||||
return Ok(vec![Toplevel::Empty]);
|
||||
}
|
||||
|
||||
let selector = selector.into_selector().remove_placeholders();
|
||||
|
||||
if selector.is_empty() {
|
||||
return Ok(vec![Toplevel::Empty]);
|
||||
}
|
||||
|
||||
let mut vals = vec![Toplevel::new_rule(selector, false)];
|
||||
|
||||
for rule in body {
|
||||
match rule {
|
||||
Stmt::RuleSet { .. } => vals.extend(self.parse_stmt(rule)?),
|
||||
Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
|
||||
Stmt::Comment(s) => vals.first_mut().unwrap().push_comment(s),
|
||||
Stmt::Media(m) => {
|
||||
let MediaRule { query, body, .. } = *m;
|
||||
vals.push(Toplevel::Media {
|
||||
query,
|
||||
body,
|
||||
inside_rule: true,
|
||||
is_group_end: false,
|
||||
});
|
||||
}
|
||||
Stmt::Supports(s) => {
|
||||
let SupportsRule { params, body } = *s;
|
||||
vals.push(Toplevel::Supports {
|
||||
params,
|
||||
body,
|
||||
inside_rule: true,
|
||||
is_group_end: false,
|
||||
});
|
||||
}
|
||||
Stmt::UnknownAtRule(u) => {
|
||||
let UnknownAtRule {
|
||||
params,
|
||||
body,
|
||||
name,
|
||||
has_body,
|
||||
..
|
||||
} = *u;
|
||||
|
||||
let at_rule = ToplevelUnknownAtRule {
|
||||
name,
|
||||
params,
|
||||
body,
|
||||
has_body,
|
||||
inside_rule: true,
|
||||
is_group_end: false,
|
||||
};
|
||||
|
||||
if has_body {
|
||||
vals.push(Toplevel::UnknownAtRule(Box::new(at_rule)));
|
||||
} else {
|
||||
vals.first_mut().unwrap().push_unknown_at_rule(at_rule);
|
||||
}
|
||||
}
|
||||
Stmt::Return(..) => unreachable!(),
|
||||
Stmt::AtRoot { body } => {
|
||||
body.into_iter().try_for_each(|r| -> SassResult<()> {
|
||||
let mut stmts = self.parse_stmt(r)?;
|
||||
|
||||
set_group_end(&mut stmts);
|
||||
|
||||
vals.append(&mut stmts);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Stmt::Keyframes(k) => {
|
||||
let Keyframes { rule, name, body } = *k;
|
||||
vals.push(Toplevel::Keyframes(Box::new(Keyframes {
|
||||
rule,
|
||||
name,
|
||||
body,
|
||||
})));
|
||||
}
|
||||
k @ Stmt::KeyframesRuleSet(..) => {
|
||||
unreachable!("@keyframes ruleset {:?}", k);
|
||||
}
|
||||
Stmt::Import(s) => self.plain_imports.push(Toplevel::Import(s)),
|
||||
};
|
||||
}
|
||||
vals
|
||||
}
|
||||
Stmt::Comment(s) => vec![Toplevel::MultilineComment(s)],
|
||||
Stmt::Import(s) => {
|
||||
self.plain_imports.push(Toplevel::Import(s));
|
||||
Vec::new()
|
||||
}
|
||||
Stmt::Style(s) => vec![Toplevel::Style(s)],
|
||||
Stmt::Media(m) => {
|
||||
let MediaRule { query, body, .. } = *m;
|
||||
vec![Toplevel::Media {
|
||||
query,
|
||||
body,
|
||||
inside_rule: false,
|
||||
is_group_end: false,
|
||||
}]
|
||||
}
|
||||
Stmt::Supports(s) => {
|
||||
let SupportsRule { params, body } = *s;
|
||||
vec![Toplevel::Supports {
|
||||
params,
|
||||
body,
|
||||
inside_rule: false,
|
||||
is_group_end: false,
|
||||
}]
|
||||
}
|
||||
Stmt::UnknownAtRule(u) => {
|
||||
let UnknownAtRule {
|
||||
params,
|
||||
body,
|
||||
name,
|
||||
has_body,
|
||||
..
|
||||
} = *u;
|
||||
vec![Toplevel::UnknownAtRule(Box::new(ToplevelUnknownAtRule {
|
||||
name,
|
||||
params,
|
||||
body,
|
||||
has_body,
|
||||
inside_rule: false,
|
||||
is_group_end: false,
|
||||
}))]
|
||||
}
|
||||
Stmt::Return(..) => unreachable!("@return: {:?}", stmt),
|
||||
Stmt::AtRoot { body } => body
|
||||
.into_iter()
|
||||
.map(|r| self.parse_stmt(r))
|
||||
.collect::<SassResult<Vec<Vec<Toplevel>>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
Stmt::Keyframes(k) => vec![Toplevel::Keyframes(k)],
|
||||
Stmt::KeyframesRuleSet(k) => {
|
||||
let KeyframesRuleSet { body, selector } = *k;
|
||||
if body.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut vals = vec![Toplevel::new_keyframes_rule(selector)];
|
||||
for rule in body {
|
||||
match rule {
|
||||
Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
|
||||
Stmt::KeyframesRuleSet(..) => vals.extend(self.parse_stmt(rule)?),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
vals
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_stylesheet(mut self, stmts: Vec<Stmt>) -> SassResult<Css> {
|
||||
for stmt in stmts {
|
||||
let mut v = self.parse_stmt(stmt)?;
|
||||
|
||||
set_group_end(&mut v);
|
||||
|
||||
self.blocks.extend(v);
|
||||
}
|
||||
|
||||
// move plain imports to top of file
|
||||
self.plain_imports.append(&mut self.blocks);
|
||||
mem::swap(&mut self.plain_imports, &mut self.blocks);
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn pretty_print(self, map: &CodeMap, style: OutputStyle) -> SassResult<String> {
|
||||
let mut buf = Vec::new();
|
||||
let allows_charset = self.allows_charset;
|
||||
match style {
|
||||
OutputStyle::Compressed => {
|
||||
CompressedFormatter::default().write_css(&mut buf, self, map)?;
|
||||
}
|
||||
OutputStyle::Expanded => {
|
||||
ExpandedFormatter::default().write_css(&mut buf, self, map)?;
|
||||
|
||||
if !buf.is_empty() {
|
||||
writeln!(buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check for this before writing
|
||||
let show_charset = allows_charset && buf.iter().any(|s| !s.is_ascii());
|
||||
let out = unsafe { String::from_utf8_unchecked(buf) };
|
||||
Ok(if show_charset {
|
||||
match style {
|
||||
OutputStyle::Compressed => format!("\u{FEFF}{}", out),
|
||||
OutputStyle::Expanded => format!("@charset \"UTF-8\";\n{}", out),
|
||||
}
|
||||
} else {
|
||||
out
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
trait Formatter {
|
||||
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct CompressedFormatter;
|
||||
|
||||
impl Formatter for CompressedFormatter {
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()> {
|
||||
for block in css.blocks {
|
||||
match block {
|
||||
Toplevel::RuleSet { selector, body, .. } => {
|
||||
if body.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut complexes = selector.0.components.iter().filter(|c| !c.is_invisible());
|
||||
if let Some(complex) = complexes.next() {
|
||||
self.write_complex(buf, complex)?;
|
||||
}
|
||||
for complex in complexes {
|
||||
write!(buf, ",")?;
|
||||
self.write_complex(buf, complex)?;
|
||||
}
|
||||
|
||||
write!(buf, "{{")?;
|
||||
self.write_block_entry(buf, &body)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::KeyframesRuleSet(selectors, styles) => {
|
||||
if styles.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut selectors = selectors.iter();
|
||||
if let Some(selector) = selectors.next() {
|
||||
write!(buf, "{}", selector)?;
|
||||
}
|
||||
for selector in selectors {
|
||||
write!(buf, ",{}", selector)?;
|
||||
}
|
||||
|
||||
write!(buf, "{{")?;
|
||||
self.write_block_entry(buf, &styles)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::Empty | Toplevel::MultilineComment(..) => continue,
|
||||
Toplevel::Import(s) => {
|
||||
write!(buf, "@import {};", s)?;
|
||||
}
|
||||
Toplevel::UnknownAtRule(u) => {
|
||||
let ToplevelUnknownAtRule {
|
||||
params, name, body, ..
|
||||
} = *u;
|
||||
|
||||
if params.is_empty() {
|
||||
write!(buf, "@{}", name)?;
|
||||
} else {
|
||||
write!(buf, "@{} {}", name, params)?;
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
write!(buf, ";")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(buf, "{{")?;
|
||||
let css = Css::from_stmts(body, AtRuleContext::Unknown, css.allows_charset)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::Keyframes(k) => {
|
||||
let Keyframes { rule, name, body } = *k;
|
||||
|
||||
write!(buf, "@{}", rule)?;
|
||||
|
||||
if !name.is_empty() {
|
||||
write!(buf, " {}", name)?;
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
write!(buf, "{{}}")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(buf, "{{")?;
|
||||
let css = Css::from_stmts(body, AtRuleContext::Keyframes, css.allows_charset)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::Supports { params, body, .. } => {
|
||||
if params.is_empty() {
|
||||
write!(buf, "@supports")?;
|
||||
} else {
|
||||
write!(buf, "@supports {}", params)?;
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
write!(buf, ";")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(buf, "{{")?;
|
||||
let css = Css::from_stmts(body, AtRuleContext::Supports, css.allows_charset)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::Media { query, body, .. } => {
|
||||
if body.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(buf, "@media {}{{", query)?;
|
||||
let css = Css::from_stmts(body, AtRuleContext::Media, css.allows_charset)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "}}")?;
|
||||
}
|
||||
Toplevel::Style(style) => {
|
||||
let value = style.value.node.to_css_string(style.value.span, true)?;
|
||||
write!(buf, "{}:{};", style.property, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// this could be a trait implemented on value itself
|
||||
#[allow(clippy::unused_self)]
|
||||
impl CompressedFormatter {
|
||||
fn write_complex(&self, buf: &mut Vec<u8>, complex: &ComplexSelector) -> SassResult<()> {
|
||||
let mut was_compound = false;
|
||||
for component in &complex.components {
|
||||
match component {
|
||||
ComplexSelectorComponent::Compound(c) if was_compound => write!(buf, " {}", c)?,
|
||||
ComplexSelectorComponent::Compound(c) => write!(buf, "{}", c)?,
|
||||
ComplexSelectorComponent::Combinator(c) => write!(buf, "{}", c)?,
|
||||
}
|
||||
was_compound = matches!(component, ComplexSelectorComponent::Compound(_));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_block_entry(&self, buf: &mut Vec<u8>, styles: &[BlockEntry]) -> SassResult<()> {
|
||||
let mut styles = styles.iter();
|
||||
|
||||
for style in &mut styles {
|
||||
match style {
|
||||
BlockEntry::Style(s) => {
|
||||
let value = s.value.node.to_css_string(s.value.span, true)?;
|
||||
write!(buf, "{}:{}", s.property, value)?;
|
||||
break;
|
||||
}
|
||||
BlockEntry::MultilineComment(..) => continue,
|
||||
b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?,
|
||||
}
|
||||
}
|
||||
|
||||
for style in styles {
|
||||
match style {
|
||||
BlockEntry::Style(s) => {
|
||||
let value = s.value.node.to_css_string(s.value.span, true)?;
|
||||
|
||||
write!(buf, ";{}:{}", s.property, value)?;
|
||||
}
|
||||
BlockEntry::MultilineComment(..) => continue,
|
||||
b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ExpandedFormatter {
|
||||
nesting: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Previous {
|
||||
is_group_end: bool,
|
||||
}
|
||||
|
||||
/// What kind of @-rule are we currently inside
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum AtRuleContext {
|
||||
Media,
|
||||
Supports,
|
||||
Keyframes,
|
||||
Unknown,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Formatter for ExpandedFormatter {
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
fn write_css(&mut self, buf: &mut Vec<u8>, css: Css, map: &CodeMap) -> SassResult<()> {
|
||||
let padding = " ".repeat(self.nesting);
|
||||
self.nesting += 1;
|
||||
|
||||
let mut prev: Option<Previous> = None;
|
||||
|
||||
for block in css.blocks {
|
||||
if block.is_invisible() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_group_end = block.is_group_end();
|
||||
|
||||
if let Some(prev) = prev {
|
||||
writeln!(buf)?;
|
||||
|
||||
if (prev.is_group_end && css.at_rule_context == AtRuleContext::None)
|
||||
|| css.at_rule_context == AtRuleContext::Supports
|
||||
{
|
||||
writeln!(buf)?;
|
||||
}
|
||||
}
|
||||
|
||||
match block {
|
||||
Toplevel::Empty => continue,
|
||||
Toplevel::RuleSet { selector, body, .. } => {
|
||||
writeln!(buf, "{}{} {{", padding, selector)?;
|
||||
|
||||
for style in body {
|
||||
writeln!(buf, "{} {}", padding, style.to_string()?)?;
|
||||
}
|
||||
|
||||
write!(buf, "{}}}", padding)?;
|
||||
}
|
||||
Toplevel::KeyframesRuleSet(selector, body) => {
|
||||
if body.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
buf,
|
||||
"{}{} {{",
|
||||
padding,
|
||||
selector
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)?;
|
||||
for style in body {
|
||||
writeln!(buf, "{} {}", padding, style.to_string()?)?;
|
||||
}
|
||||
write!(buf, "{}}}", padding)?;
|
||||
}
|
||||
Toplevel::MultilineComment(s) => {
|
||||
write!(buf, "{}/*{}*/", padding, s)?;
|
||||
}
|
||||
Toplevel::Import(s) => {
|
||||
write!(buf, "{}@import {};", padding, s)?;
|
||||
}
|
||||
Toplevel::UnknownAtRule(u) => {
|
||||
let ToplevelUnknownAtRule {
|
||||
params,
|
||||
name,
|
||||
body,
|
||||
has_body,
|
||||
inside_rule,
|
||||
..
|
||||
} = *u;
|
||||
|
||||
if params.is_empty() {
|
||||
write!(buf, "{}@{}", padding, name)?;
|
||||
} else {
|
||||
write!(buf, "{}@{} {}", padding, name, params)?;
|
||||
}
|
||||
|
||||
let css = Css::from_stmts(
|
||||
body,
|
||||
if inside_rule {
|
||||
AtRuleContext::Unknown
|
||||
} else {
|
||||
AtRuleContext::None
|
||||
},
|
||||
css.allows_charset,
|
||||
)?;
|
||||
|
||||
if !has_body {
|
||||
write!(buf, ";")?;
|
||||
prev = Some(Previous { is_group_end });
|
||||
continue;
|
||||
}
|
||||
|
||||
if css.blocks.iter().all(Toplevel::is_invisible) {
|
||||
write!(buf, " {{}}")?;
|
||||
prev = Some(Previous { is_group_end });
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(buf, " {{")?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "\n{}}}", padding)?;
|
||||
}
|
||||
Toplevel::Keyframes(k) => {
|
||||
let Keyframes { rule, name, body } = *k;
|
||||
|
||||
write!(buf, "{}@{}", padding, rule)?;
|
||||
|
||||
if !name.is_empty() {
|
||||
write!(buf, " {}", name)?;
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
write!(buf, " {{}}")?;
|
||||
prev = Some(Previous { is_group_end });
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(buf, " {{")?;
|
||||
let css = Css::from_stmts(body, AtRuleContext::Keyframes, css.allows_charset)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "\n{}}}", padding)?;
|
||||
}
|
||||
Toplevel::Supports {
|
||||
params,
|
||||
body,
|
||||
inside_rule,
|
||||
..
|
||||
} => {
|
||||
if params.is_empty() {
|
||||
write!(buf, "{}@supports", padding)?;
|
||||
} else {
|
||||
write!(buf, "{}@supports {}", padding, params)?;
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
write!(buf, ";")?;
|
||||
prev = Some(Previous { is_group_end });
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(buf, " {{")?;
|
||||
let css = Css::from_stmts(
|
||||
body,
|
||||
if inside_rule {
|
||||
AtRuleContext::Supports
|
||||
} else {
|
||||
AtRuleContext::None
|
||||
},
|
||||
css.allows_charset,
|
||||
)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "\n{}}}", padding)?;
|
||||
}
|
||||
Toplevel::Media {
|
||||
query,
|
||||
body,
|
||||
inside_rule,
|
||||
..
|
||||
} => {
|
||||
writeln!(buf, "{}@media {} {{", padding, query)?;
|
||||
let css = Css::from_stmts(
|
||||
body,
|
||||
if inside_rule {
|
||||
AtRuleContext::Media
|
||||
} else {
|
||||
AtRuleContext::None
|
||||
},
|
||||
css.allows_charset,
|
||||
)?;
|
||||
self.write_css(buf, css, map)?;
|
||||
write!(buf, "\n{}}}", padding)?;
|
||||
}
|
||||
Toplevel::Style(s) => {
|
||||
write!(buf, "{}{}", padding, s.to_string()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
prev = Some(Previous { is_group_end });
|
||||
}
|
||||
|
||||
self.nesting -= 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,410 +0,0 @@
|
||||
use std::{collections::HashMap, mem};
|
||||
|
||||
use codemap::Span;
|
||||
|
||||
use crate::{
|
||||
args::{CallArg, CallArgs, FuncArg, FuncArgs},
|
||||
common::QuoteKind,
|
||||
error::SassResult,
|
||||
scope::Scope,
|
||||
utils::{read_until_closing_paren, read_until_closing_quote, read_until_newline},
|
||||
value::Value,
|
||||
Token,
|
||||
};
|
||||
|
||||
use super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn parse_func_args(&mut self) -> SassResult<FuncArgs> {
|
||||
let mut args: Vec<FuncArg> = Vec::new();
|
||||
let mut close_paren_span: Span = match self.toks.peek() {
|
||||
Some(Token { pos, .. }) => pos,
|
||||
None => return Err(("expected \")\".", self.span_before).into()),
|
||||
};
|
||||
|
||||
self.whitespace_or_comment();
|
||||
while let Some(Token { kind, pos }) = self.toks.next() {
|
||||
let name = match kind {
|
||||
'$' => self.parse_identifier_no_interpolation(false)?,
|
||||
')' => {
|
||||
close_paren_span = pos;
|
||||
break;
|
||||
}
|
||||
_ => return Err(("expected \")\".", pos).into()),
|
||||
};
|
||||
let mut default: Vec<Token> = Vec::new();
|
||||
let mut is_variadic = false;
|
||||
self.whitespace_or_comment();
|
||||
let (kind, span) = match self.toks.next() {
|
||||
Some(Token { kind, pos }) => (kind, pos),
|
||||
None => return Err(("expected \")\".", pos).into()),
|
||||
};
|
||||
match kind {
|
||||
':' => {
|
||||
self.whitespace_or_comment();
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match &tok.kind {
|
||||
',' => {
|
||||
self.toks.next();
|
||||
self.whitespace_or_comment();
|
||||
args.push(FuncArg {
|
||||
name: name.node.into(),
|
||||
default: Some(default),
|
||||
is_variadic,
|
||||
});
|
||||
break;
|
||||
}
|
||||
')' => {
|
||||
args.push(FuncArg {
|
||||
name: name.node.into(),
|
||||
default: Some(default),
|
||||
is_variadic,
|
||||
});
|
||||
close_paren_span = tok.pos();
|
||||
break;
|
||||
}
|
||||
'(' => {
|
||||
default.push(self.toks.next().unwrap());
|
||||
default.extend(read_until_closing_paren(self.toks)?);
|
||||
}
|
||||
'/' => {
|
||||
let next = self.toks.next().unwrap();
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '/', .. }) => read_until_newline(self.toks),
|
||||
_ => default.push(next),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
&q @ '"' | &q @ '\'' => {
|
||||
default.push(self.toks.next().unwrap());
|
||||
default.extend(read_until_closing_quote(self.toks, q)?);
|
||||
continue;
|
||||
}
|
||||
'\\' => {
|
||||
default.push(self.toks.next().unwrap());
|
||||
default.push(match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => continue,
|
||||
});
|
||||
}
|
||||
_ => default.push(self.toks.next().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
self.expect_char('.')?;
|
||||
self.expect_char('.')?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
self.expect_char(')')?;
|
||||
|
||||
is_variadic = true;
|
||||
|
||||
args.push(FuncArg {
|
||||
name: name.node.into(),
|
||||
// todo: None if empty
|
||||
default: Some(default),
|
||||
is_variadic,
|
||||
});
|
||||
break;
|
||||
}
|
||||
')' => {
|
||||
close_paren_span = span;
|
||||
args.push(FuncArg {
|
||||
name: name.node.into(),
|
||||
default: if default.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(default)
|
||||
},
|
||||
is_variadic,
|
||||
});
|
||||
break;
|
||||
}
|
||||
',' => args.push(FuncArg {
|
||||
name: name.node.into(),
|
||||
default: None,
|
||||
is_variadic,
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
self.whitespace_or_comment();
|
||||
}
|
||||
self.whitespace_or_comment();
|
||||
// TODO: this should NOT eat the opening curly brace
|
||||
// todo: self.expect_char('{')?;
|
||||
match self.toks.next() {
|
||||
Some(v) if v.kind == '{' => {}
|
||||
Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()),
|
||||
};
|
||||
Ok(FuncArgs(args))
|
||||
}
|
||||
|
||||
pub(super) fn parse_call_args(&mut self) -> SassResult<CallArgs> {
|
||||
let mut args = HashMap::new();
|
||||
self.whitespace_or_comment();
|
||||
let mut name = String::new();
|
||||
|
||||
let mut span = self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or(("expected \")\".", self.span_before))?
|
||||
.pos();
|
||||
|
||||
loop {
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if self.consume_char_if_exists(')') {
|
||||
return Ok(CallArgs(args, span));
|
||||
}
|
||||
|
||||
if self.consume_char_if_exists(',') {
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if self.consume_char_if_exists(',') {
|
||||
return Err(("expected \")\".", self.span_before).into());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(Token { kind: '$', pos }) = self.toks.peek() {
|
||||
let start = self.toks.cursor();
|
||||
|
||||
span = span.merge(pos);
|
||||
self.toks.next();
|
||||
|
||||
let v = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if self.consume_char_if_exists(':') {
|
||||
name = v.node;
|
||||
} else {
|
||||
self.toks.set_cursor(start);
|
||||
name.clear();
|
||||
}
|
||||
} else {
|
||||
name.clear();
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let value = self.parse_value(true, &|parser| match parser.toks.peek() {
|
||||
Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true,
|
||||
Some(Token { kind: '.', .. }) => {
|
||||
let next_is_dot =
|
||||
matches!(parser.toks.peek_n(1), Some(Token { kind: '.', .. }));
|
||||
|
||||
next_is_dot
|
||||
}
|
||||
Some(Token { kind: '=', .. }) => {
|
||||
let next_is_eq = matches!(parser.toks.peek_n(1), Some(Token { kind: '=', .. }));
|
||||
|
||||
!next_is_eq
|
||||
}
|
||||
Some(..) | None => false,
|
||||
});
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: ')', .. }) => {
|
||||
self.toks.next();
|
||||
args.insert(
|
||||
if name.is_empty() {
|
||||
CallArg::Positional(args.len())
|
||||
} else {
|
||||
CallArg::Named(mem::take(&mut name).into())
|
||||
},
|
||||
value,
|
||||
);
|
||||
return Ok(CallArgs(args, span));
|
||||
}
|
||||
Some(Token { kind: ',', .. }) => {
|
||||
self.toks.next();
|
||||
args.insert(
|
||||
if name.is_empty() {
|
||||
CallArg::Positional(args.len())
|
||||
} else {
|
||||
CallArg::Named(mem::take(&mut name).into())
|
||||
},
|
||||
value,
|
||||
);
|
||||
self.whitespace_or_comment();
|
||||
if self.consume_char_if_exists(',') {
|
||||
return Err(("expected \")\".", self.span_before).into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Some(Token { kind: '.', pos }) => {
|
||||
self.toks.next();
|
||||
|
||||
if let Some(Token { kind: '.', pos }) = self.toks.peek() {
|
||||
if !name.is_empty() {
|
||||
return Err(("expected \")\".", pos).into());
|
||||
}
|
||||
self.toks.next();
|
||||
self.expect_char('.')?;
|
||||
} else {
|
||||
return Err(("expected \")\".", pos).into());
|
||||
}
|
||||
|
||||
let val = value?;
|
||||
match val.node {
|
||||
Value::ArgList(v) => {
|
||||
for arg in v {
|
||||
args.insert(CallArg::Positional(args.len()), Ok(arg));
|
||||
}
|
||||
}
|
||||
Value::List(v, ..) => {
|
||||
for arg in v {
|
||||
args.insert(
|
||||
CallArg::Positional(args.len()),
|
||||
Ok(arg.span(val.span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Map(v) => {
|
||||
// NOTE: we clone the map here because it is used
|
||||
// later for error reporting. perhaps there is
|
||||
// some way around this?
|
||||
for (name, arg) in v.clone().entries() {
|
||||
let name = match name {
|
||||
Value::String(s, ..) => s,
|
||||
_ => {
|
||||
return Err((
|
||||
format!(
|
||||
"{} is not a string in {}.",
|
||||
name.inspect(val.span)?,
|
||||
Value::Map(v).inspect(val.span)?
|
||||
),
|
||||
val.span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
args.insert(CallArg::Named(name.into()), Ok(arg.span(val.span)));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
args.insert(CallArg::Positional(args.len()), Ok(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Token { kind: '=', .. }) => {
|
||||
self.toks.next();
|
||||
let left = value?;
|
||||
|
||||
let right = self.parse_value(true, &|parser| match parser.toks.peek() {
|
||||
Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true,
|
||||
Some(Token { kind: '.', .. }) => {
|
||||
let next_is_dot =
|
||||
matches!(parser.toks.peek_n(1), Some(Token { kind: '.', .. }));
|
||||
|
||||
next_is_dot
|
||||
}
|
||||
Some(..) | None => false,
|
||||
})?;
|
||||
|
||||
let value_span = left.span.merge(right.span);
|
||||
span = span.merge(value_span);
|
||||
|
||||
let value = format!(
|
||||
"{}={}",
|
||||
left.node
|
||||
.to_css_string(left.span, self.options.is_compressed())?,
|
||||
right
|
||||
.node
|
||||
.to_css_string(right.span, self.options.is_compressed())?
|
||||
);
|
||||
|
||||
args.insert(
|
||||
if name.is_empty() {
|
||||
CallArg::Positional(args.len())
|
||||
} else {
|
||||
CallArg::Named(mem::take(&mut name).into())
|
||||
},
|
||||
Ok(Value::String(value, QuoteKind::None).span(value_span)),
|
||||
);
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: ')', .. }) => {
|
||||
self.toks.next();
|
||||
return Ok(CallArgs(args, span));
|
||||
}
|
||||
Some(Token { kind: ',', pos }) => {
|
||||
span = span.merge(pos);
|
||||
self.toks.next();
|
||||
self.whitespace_or_comment();
|
||||
continue;
|
||||
}
|
||||
Some(Token { kind: '.', .. }) => {
|
||||
self.toks.next();
|
||||
|
||||
self.expect_char('.')?;
|
||||
|
||||
if !name.is_empty() {
|
||||
return Err(("expected \")\".", self.span_before).into());
|
||||
}
|
||||
|
||||
self.expect_char('.')?;
|
||||
}
|
||||
Some(Token { pos, .. }) => {
|
||||
return Err(("expected \")\".", pos).into());
|
||||
}
|
||||
None => return Err(("expected \")\".", span).into()),
|
||||
}
|
||||
}
|
||||
Some(Token { pos, .. }) => {
|
||||
value?;
|
||||
return Err(("expected \")\".", pos).into());
|
||||
}
|
||||
None => return Err(("expected \")\".", span).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn eval_args(
|
||||
&mut self,
|
||||
fn_args: &FuncArgs,
|
||||
mut args: CallArgs,
|
||||
) -> SassResult<Scope> {
|
||||
let mut scope = Scope::new();
|
||||
if fn_args.0.is_empty() {
|
||||
args.max_args(0)?;
|
||||
return Ok(scope);
|
||||
}
|
||||
|
||||
if !fn_args.0.iter().any(|arg| arg.is_variadic) {
|
||||
args.max_args(fn_args.len())?;
|
||||
}
|
||||
|
||||
self.scopes.enter_new_scope();
|
||||
for (idx, arg) in fn_args.0.iter().enumerate() {
|
||||
if arg.is_variadic {
|
||||
let arg_list = Value::ArgList(args.get_variadic()?);
|
||||
scope.insert_var(arg.name, arg_list);
|
||||
break;
|
||||
}
|
||||
|
||||
let val = match args.get(idx, arg.name) {
|
||||
Some(v) => v,
|
||||
None => match arg.default.as_ref() {
|
||||
Some(v) => self.parse_value_from_vec(v, true),
|
||||
None => {
|
||||
return Err(
|
||||
(format!("Missing argument ${}.", &arg.name), args.span()).into()
|
||||
)
|
||||
}
|
||||
},
|
||||
}?
|
||||
.node;
|
||||
self.scopes.insert_var_last(arg.name, val.clone());
|
||||
scope.insert_var(arg.name, val);
|
||||
}
|
||||
self.scopes.exit_scope();
|
||||
Ok(scope)
|
||||
}
|
||||
}
|
55
src/parse/at_root_query.rs
Normal file
55
src/parse/at_root_query.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{ast::AtRootQuery, error::SassResult, lexer::Lexer};
|
||||
|
||||
use super::BaseParser;
|
||||
|
||||
pub(crate) struct AtRootQueryParser<'a> {
|
||||
toks: Lexer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BaseParser<'a> for AtRootQueryParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AtRootQueryParser<'a> {
|
||||
pub fn new(toks: Lexer<'a>) -> AtRootQueryParser<'a> {
|
||||
AtRootQueryParser { toks }
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> SassResult<AtRootQuery> {
|
||||
self.expect_char('(')?;
|
||||
self.whitespace()?;
|
||||
let include = self.scan_identifier("with", false)?;
|
||||
|
||||
if !include {
|
||||
self.expect_identifier("without", false)?;
|
||||
}
|
||||
|
||||
self.whitespace()?;
|
||||
self.expect_char(':')?;
|
||||
self.whitespace()?;
|
||||
|
||||
let mut names = HashSet::new();
|
||||
|
||||
loop {
|
||||
names.insert(self.parse_identifier(false, false)?.to_ascii_lowercase());
|
||||
self.whitespace()?;
|
||||
|
||||
if !self.looking_at_identifier() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_char(')')?;
|
||||
self.expect_done()?;
|
||||
|
||||
Ok(AtRootQuery::new(include, names))
|
||||
}
|
||||
}
|
695
src/parse/base.rs
Normal file
695
src/parse/base.rs
Normal file
@ -0,0 +1,695 @@
|
||||
use crate::{
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
utils::{as_hex, hex_char_for, is_name, is_name_start, opposite_bracket},
|
||||
Token,
|
||||
};
|
||||
|
||||
// todo: can we simplify lifetimes (by maybe not storing reference to lexer)
|
||||
pub(crate) trait BaseParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a>;
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a>;
|
||||
|
||||
fn whitespace_without_comments(&mut self) {
|
||||
while matches!(
|
||||
self.toks().peek(),
|
||||
Some(Token {
|
||||
kind: ' ' | '\t' | '\n',
|
||||
..
|
||||
})
|
||||
) {
|
||||
self.toks_mut().next();
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(&mut self) -> SassResult<()> {
|
||||
loop {
|
||||
self.whitespace_without_comments();
|
||||
|
||||
if !self.scan_comment()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan_comment(&mut self) -> SassResult<bool> {
|
||||
if !matches!(self.toks().peek(), Some(Token { kind: '/', .. })) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(match self.toks().peek_n(1) {
|
||||
Some(Token { kind: '/', .. }) => {
|
||||
self.skip_silent_comment()?;
|
||||
true
|
||||
}
|
||||
Some(Token { kind: '*', .. }) => {
|
||||
self.skip_loud_comment()?;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn skip_silent_comment(&mut self) -> SassResult<()> {
|
||||
debug_assert!(self.next_matches("//"));
|
||||
self.toks_mut().next();
|
||||
self.toks_mut().next();
|
||||
while self.toks().peek().is_some() && !self.toks().next_char_is('\n') {
|
||||
self.toks_mut().next();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_matches(&mut self, s: &str) -> bool {
|
||||
for (idx, c) in s.chars().enumerate() {
|
||||
match self.toks().peek_n(idx) {
|
||||
Some(Token { kind, .. }) if kind == c => {}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn skip_loud_comment(&mut self) -> SassResult<()> {
|
||||
debug_assert!(self.next_matches("/*"));
|
||||
self.toks_mut().next();
|
||||
self.toks_mut().next();
|
||||
|
||||
while let Some(next) = self.toks_mut().next() {
|
||||
if next.kind != '*' {
|
||||
continue;
|
||||
}
|
||||
|
||||
while self.scan_char('*') {}
|
||||
|
||||
if self.scan_char('/') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(("expected more input.", self.toks().current_span()).into())
|
||||
}
|
||||
|
||||
fn scan_char(&mut self, c: char) -> bool {
|
||||
if let Some(Token { kind, .. }) = self.toks().peek() {
|
||||
if kind == c {
|
||||
self.toks_mut().next();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn scan(&mut self, s: &str) -> bool {
|
||||
let start = self.toks().cursor();
|
||||
for c in s.chars() {
|
||||
if !self.scan_char(c) {
|
||||
self.toks_mut().set_cursor(start);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn expect_whitespace(&mut self) -> SassResult<()> {
|
||||
if !matches!(
|
||||
self.toks().peek(),
|
||||
Some(Token {
|
||||
kind: ' ' | '\t' | '\n' | '\r',
|
||||
..
|
||||
})
|
||||
) && !self.scan_comment()?
|
||||
{
|
||||
return Err(("Expected whitespace.", self.toks().current_span()).into());
|
||||
}
|
||||
|
||||
self.whitespace()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_identifier(
|
||||
&mut self,
|
||||
// default=false
|
||||
normalize: bool,
|
||||
// default=false
|
||||
unit: bool,
|
||||
) -> SassResult<String> {
|
||||
let mut text = String::new();
|
||||
|
||||
if self.scan_char('-') {
|
||||
text.push('-');
|
||||
|
||||
if self.scan_char('-') {
|
||||
text.push('-');
|
||||
self.parse_identifier_body(&mut text, normalize, unit)?;
|
||||
return Ok(text);
|
||||
}
|
||||
}
|
||||
|
||||
match self.toks().peek() {
|
||||
Some(Token { kind: '_', .. }) if normalize => {
|
||||
self.toks_mut().next();
|
||||
text.push('-');
|
||||
}
|
||||
Some(Token { kind, .. }) if is_name_start(kind) => {
|
||||
self.toks_mut().next();
|
||||
text.push(kind);
|
||||
}
|
||||
Some(Token { kind: '\\', .. }) => {
|
||||
text.push_str(&self.parse_escape(true)?);
|
||||
}
|
||||
Some(..) | None => {
|
||||
return Err(("Expected identifier.", self.toks().current_span()).into())
|
||||
}
|
||||
}
|
||||
|
||||
self.parse_identifier_body(&mut text, normalize, unit)?;
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn parse_identifier_body(
|
||||
&mut self,
|
||||
buffer: &mut String,
|
||||
normalize: bool,
|
||||
unit: bool,
|
||||
) -> SassResult<()> {
|
||||
while let Some(tok) = self.toks().peek() {
|
||||
if unit && tok.kind == '-' {
|
||||
// Disallow `-` followed by a dot or a digit digit in units.
|
||||
let second = match self.toks().peek_n(1) {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if second.kind == '.' || second.kind.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks_mut().next();
|
||||
buffer.push('-');
|
||||
} else if normalize && tok.kind == '_' {
|
||||
buffer.push('-');
|
||||
self.toks_mut().next();
|
||||
} else if is_name(tok.kind) {
|
||||
self.toks_mut().next();
|
||||
buffer.push(tok.kind);
|
||||
} else if tok.kind == '\\' {
|
||||
buffer.push_str(&self.parse_escape(false)?);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_escape(&mut self, identifier_start: bool) -> SassResult<String> {
|
||||
self.expect_char('\\')?;
|
||||
let mut value = 0;
|
||||
let first = match self.toks().peek() {
|
||||
Some(t) => t,
|
||||
None => return Err(("Expected expression.", self.toks().current_span()).into()),
|
||||
};
|
||||
let mut span = first.pos();
|
||||
if first.kind == '\n' {
|
||||
return Err(("Expected escape sequence.", span).into());
|
||||
} else if first.kind.is_ascii_hexdigit() {
|
||||
for _ in 0..6 {
|
||||
let next = match self.toks().peek() {
|
||||
Some(t) => t,
|
||||
None => break,
|
||||
};
|
||||
if !next.kind.is_ascii_hexdigit() {
|
||||
break;
|
||||
}
|
||||
value *= 16;
|
||||
span = span.merge(next.pos);
|
||||
value += as_hex(next.kind);
|
||||
self.toks_mut().next();
|
||||
}
|
||||
if matches!(
|
||||
self.toks().peek(),
|
||||
Some(Token { kind: ' ', .. })
|
||||
| Some(Token { kind: '\n', .. })
|
||||
| Some(Token { kind: '\t', .. })
|
||||
) {
|
||||
self.toks_mut().next();
|
||||
}
|
||||
} else {
|
||||
span = span.merge(first.pos);
|
||||
value = first.kind as u32;
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
let c = std::char::from_u32(value).ok_or(("Invalid Unicode code point.", span))?;
|
||||
if (identifier_start && is_name_start(c) && !c.is_ascii_digit())
|
||||
|| (!identifier_start && is_name(c))
|
||||
{
|
||||
Ok(c.to_string())
|
||||
} else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_ascii_digit()) {
|
||||
let mut buf = String::with_capacity(4);
|
||||
buf.push('\\');
|
||||
if value > 0xF {
|
||||
buf.push(hex_char_for(value >> 4));
|
||||
}
|
||||
buf.push(hex_char_for(value & 0xF));
|
||||
buf.push(' ');
|
||||
Ok(buf)
|
||||
} else {
|
||||
Ok(format!("\\{}", c))
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_char(&mut self, c: char) -> SassResult<()> {
|
||||
match self.toks().peek() {
|
||||
Some(tok) if tok.kind == c => {
|
||||
self.toks_mut().next();
|
||||
Ok(())
|
||||
}
|
||||
Some(Token { pos, .. }) => Err((format!("expected \"{}\".", c), pos).into()),
|
||||
None => Err((format!("expected \"{}\".", c), self.toks().current_span()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_char_with_message(&mut self, c: char, msg: &'static str) -> SassResult<()> {
|
||||
match self.toks().peek() {
|
||||
Some(tok) if tok.kind == c => {
|
||||
self.toks_mut().next();
|
||||
Ok(())
|
||||
}
|
||||
Some(Token { pos, .. }) => Err((format!("expected {}.", msg), pos).into()),
|
||||
None => Err((format!("expected {}.", msg), self.toks().prev_span()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> SassResult<String> {
|
||||
let quote = match self.toks_mut().next() {
|
||||
Some(Token {
|
||||
kind: q @ ('\'' | '"'),
|
||||
..
|
||||
}) => q,
|
||||
Some(Token { pos, .. }) => return Err(("Expected string.", pos).into()),
|
||||
None => return Err(("Expected string.", self.toks().current_span()).into()),
|
||||
};
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut found_matching_quote = false;
|
||||
|
||||
while let Some(next) = self.toks().peek() {
|
||||
if next.kind == quote {
|
||||
self.toks_mut().next();
|
||||
found_matching_quote = true;
|
||||
break;
|
||||
} else if next.kind == '\n' || next.kind == '\r' {
|
||||
break;
|
||||
} else if next.kind == '\\' {
|
||||
if matches!(
|
||||
self.toks().peek_n(1),
|
||||
Some(Token {
|
||||
kind: '\n' | '\r',
|
||||
..
|
||||
})
|
||||
) {
|
||||
self.toks_mut().next();
|
||||
self.toks_mut().next();
|
||||
} else {
|
||||
buffer.push(self.consume_escaped_char()?);
|
||||
}
|
||||
} else {
|
||||
self.toks_mut().next();
|
||||
buffer.push(next.kind);
|
||||
}
|
||||
}
|
||||
|
||||
if !found_matching_quote {
|
||||
return Err((format!("Expected {quote}."), self.toks().current_span()).into());
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn consume_escaped_char(&mut self) -> SassResult<char> {
|
||||
self.expect_char('\\')?;
|
||||
|
||||
match self.toks().peek() {
|
||||
None => Ok('\u{FFFD}'),
|
||||
Some(Token {
|
||||
kind: '\n' | '\r',
|
||||
pos,
|
||||
}) => Err(("Expected escape sequence.", pos).into()),
|
||||
Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => {
|
||||
let mut value = 0;
|
||||
for _ in 0..6 {
|
||||
let next = match self.toks().peek() {
|
||||
Some(c) => c,
|
||||
None => break,
|
||||
};
|
||||
if !next.kind.is_ascii_hexdigit() {
|
||||
break;
|
||||
}
|
||||
self.toks_mut().next();
|
||||
value = (value << 4) + as_hex(next.kind);
|
||||
}
|
||||
|
||||
if self.toks().peek().is_some()
|
||||
&& self.toks().peek().unwrap().kind.is_ascii_whitespace()
|
||||
{
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF {
|
||||
Ok('\u{FFFD}')
|
||||
} else {
|
||||
Ok(char::from_u32(value).unwrap())
|
||||
}
|
||||
}
|
||||
Some(Token { kind, .. }) => {
|
||||
self.toks_mut().next();
|
||||
Ok(kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn declaration_value(&mut self, allow_empty: bool) -> SassResult<String> {
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut brackets = Vec::new();
|
||||
let mut wrote_newline = false;
|
||||
|
||||
while let Some(tok) = self.toks().peek() {
|
||||
match tok.kind {
|
||||
'\\' => {
|
||||
self.toks_mut().next();
|
||||
buffer.push_str(&self.parse_escape(true)?);
|
||||
wrote_newline = false;
|
||||
}
|
||||
'"' | '\'' => {
|
||||
buffer.push_str(&self.fallible_raw_text(Self::parse_string)?);
|
||||
wrote_newline = false;
|
||||
}
|
||||
'/' => {
|
||||
if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) {
|
||||
buffer.push_str(&self.fallible_raw_text(Self::skip_loud_comment)?);
|
||||
} else {
|
||||
buffer.push('/');
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
'#' => {
|
||||
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
|
||||
let s = self.parse_identifier(false, false)?;
|
||||
buffer.push_str(&s);
|
||||
} else {
|
||||
buffer.push('#');
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
c @ (' ' | '\t') => {
|
||||
if wrote_newline
|
||||
|| !self
|
||||
.toks()
|
||||
.peek_n(1)
|
||||
.map_or(false, |tok| tok.kind.is_ascii_whitespace())
|
||||
{
|
||||
buffer.push(c);
|
||||
}
|
||||
|
||||
self.toks_mut().next();
|
||||
}
|
||||
'\n' | '\r' => {
|
||||
if !wrote_newline {
|
||||
buffer.push('\n');
|
||||
}
|
||||
|
||||
wrote_newline = true;
|
||||
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
'[' | '(' | '{' => {
|
||||
buffer.push(tok.kind);
|
||||
|
||||
self.toks_mut().next();
|
||||
|
||||
brackets.push(opposite_bracket(tok.kind));
|
||||
wrote_newline = false;
|
||||
}
|
||||
']' | ')' | '}' => {
|
||||
if let Some(end) = brackets.pop() {
|
||||
buffer.push(tok.kind);
|
||||
self.expect_char(end)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
';' => {
|
||||
if brackets.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks_mut().next();
|
||||
buffer.push(';');
|
||||
wrote_newline = false;
|
||||
}
|
||||
'u' | 'U' => {
|
||||
if let Some(url) = self.try_parse_url()? {
|
||||
buffer.push_str(&url);
|
||||
} else {
|
||||
buffer.push(tok.kind);
|
||||
self.toks_mut().next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
c => {
|
||||
if self.looking_at_identifier() {
|
||||
buffer.push_str(&self.parse_identifier(false, false)?);
|
||||
} else {
|
||||
self.toks_mut().next();
|
||||
buffer.push(c);
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last) = brackets.pop() {
|
||||
self.expect_char(last)?;
|
||||
}
|
||||
|
||||
if !allow_empty && buffer.is_empty() {
|
||||
return Err(("Expected token.", self.toks().current_span()).into());
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Returns whether the scanner is immediately before a plain CSS identifier.
|
||||
///
|
||||
/// This is based on [the CSS algorithm][], but it assumes all backslashes
|
||||
/// start escapes.
|
||||
///
|
||||
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
|
||||
fn looking_at_identifier(&self) -> bool {
|
||||
match self.toks().peek() {
|
||||
Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true,
|
||||
Some(Token { kind: '-', .. }) => {}
|
||||
Some(..) | None => return false,
|
||||
}
|
||||
|
||||
match self.toks().peek_n(1) {
|
||||
Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => true,
|
||||
Some(..) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_parse_url(&mut self) -> SassResult<Option<String>> {
|
||||
let start = self.toks().cursor();
|
||||
|
||||
if !self.scan_identifier("url", false)? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !self.scan_char('(') {
|
||||
self.toks_mut().set_cursor(start);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.whitespace()?;
|
||||
|
||||
// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
|
||||
// backtrack and re-parse as a function expression.
|
||||
let mut buffer = "url(".to_owned();
|
||||
|
||||
while let Some(next) = self.toks().peek() {
|
||||
match next.kind {
|
||||
'\\' => {
|
||||
buffer.push_str(&self.parse_escape(false)?);
|
||||
}
|
||||
'!' | '#' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => {
|
||||
self.toks_mut().next();
|
||||
buffer.push(next.kind);
|
||||
}
|
||||
')' => {
|
||||
self.toks_mut().next();
|
||||
buffer.push(next.kind);
|
||||
|
||||
return Ok(Some(buffer));
|
||||
}
|
||||
' ' | '\t' | '\n' | '\r' => {
|
||||
self.whitespace_without_comments();
|
||||
|
||||
if !self.toks().next_char_is(')') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
self.toks_mut().set_cursor(start);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn raw_text<T>(&mut self, func: impl Fn(&mut Self) -> T) -> String {
|
||||
let start = self.toks().cursor();
|
||||
func(self);
|
||||
self.toks().raw_text(start)
|
||||
}
|
||||
|
||||
fn fallible_raw_text<T>(
|
||||
&mut self,
|
||||
func: impl Fn(&mut Self) -> SassResult<T>,
|
||||
) -> SassResult<String> {
|
||||
let start = self.toks().cursor();
|
||||
func(self)?;
|
||||
Ok(self.toks().raw_text(start))
|
||||
}
|
||||
|
||||
/// Peeks to see if the `ident` is at the current position. If it is,
|
||||
/// consume the identifier
|
||||
fn scan_identifier(
|
||||
&mut self,
|
||||
ident: &'static str,
|
||||
// default=false
|
||||
case_sensitive: bool,
|
||||
) -> SassResult<bool> {
|
||||
if !self.looking_at_identifier() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let start = self.toks().cursor();
|
||||
|
||||
if self.consume_identifier(ident, case_sensitive)? && !self.looking_at_identifier_body() {
|
||||
Ok(true)
|
||||
} else {
|
||||
self.toks_mut().set_cursor(start);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<bool> {
|
||||
for c in ident.chars() {
|
||||
if !self.scan_ident_char(c, case_sensitive)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn scan_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<bool> {
|
||||
let matches = |actual: char| {
|
||||
if case_sensitive {
|
||||
actual == c
|
||||
} else {
|
||||
actual.to_ascii_lowercase() == c.to_ascii_lowercase()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(match self.toks().peek() {
|
||||
Some(Token { kind, .. }) if matches(kind) => {
|
||||
self.toks_mut().next();
|
||||
true
|
||||
}
|
||||
Some(Token { kind: '\\', .. }) => {
|
||||
let start = self.toks().cursor();
|
||||
if matches(self.consume_escaped_char()?) {
|
||||
return Ok(true);
|
||||
}
|
||||
self.toks_mut().set_cursor(start);
|
||||
false
|
||||
}
|
||||
Some(..) | None => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn expect_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<()> {
|
||||
if self.scan_ident_char(c, case_sensitive)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err((format!("Expected \"{}\".", c), self.toks().current_span()).into())
|
||||
}
|
||||
|
||||
fn looking_at_identifier_body(&mut self) -> bool {
|
||||
matches!(self.toks().peek(), Some(t) if is_name(t.kind) || t.kind == '\\')
|
||||
}
|
||||
|
||||
fn parse_variable_name(&mut self) -> SassResult<String> {
|
||||
self.expect_char('$')?;
|
||||
self.parse_identifier(true, false)
|
||||
}
|
||||
|
||||
fn expect_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<()> {
|
||||
let start = self.toks().cursor();
|
||||
|
||||
for c in ident.chars() {
|
||||
if !self.scan_ident_char(c, case_sensitive)? {
|
||||
return Err((
|
||||
format!("Expected \"{}\".", ident),
|
||||
self.toks_mut().span_from(start),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
if !self.looking_at_identifier_body() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err((
|
||||
format!("Expected \"{}\".", ident),
|
||||
self.toks_mut().span_from(start),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
// todo: not real impl
|
||||
fn expect_done(&mut self) -> SassResult<()> {
|
||||
debug_assert!(self.toks().peek().is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spaces(&mut self) {
|
||||
while self.toks().next_char_is(' ') || self.toks().next_char_is('\t') {
|
||||
self.toks_mut().next();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
use std::ops::{BitAnd, BitOr};
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{common::Identifier, interner::InternedString, value::Value};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct NeverEmptyVec<T> {
|
||||
first: T,
|
||||
rest: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> NeverEmptyVec<T> {
|
||||
pub const fn new(first: T) -> Self {
|
||||
Self {
|
||||
first,
|
||||
rest: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last(&self) -> &T {
|
||||
self.rest.last().unwrap_or(&self.first)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) {
|
||||
self.rest.push(value);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
self.rest.pop()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rest.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// A toplevel element beginning with something other than
|
||||
/// `$`, `@`, `/`, whitespace, or a control character is either a
|
||||
/// selector or a style.
|
||||
#[derive(Debug)]
|
||||
pub(super) enum SelectorOrStyle {
|
||||
Selector(String),
|
||||
Style(InternedString, Option<Box<Spanned<Value>>>),
|
||||
ModuleVariableRedeclaration(Identifier),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct ContextFlags(u8);
|
||||
|
||||
pub(crate) struct ContextFlag(u8);
|
||||
|
||||
impl ContextFlags {
|
||||
pub const IN_MIXIN: ContextFlag = ContextFlag(1);
|
||||
pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1);
|
||||
pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2);
|
||||
pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3);
|
||||
pub const IN_AT_ROOT_RULE: ContextFlag = ContextFlag(1 << 4);
|
||||
|
||||
pub const fn empty() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn in_mixin(self) -> bool {
|
||||
(self.0 & Self::IN_MIXIN) != 0
|
||||
}
|
||||
|
||||
pub fn in_function(self) -> bool {
|
||||
(self.0 & Self::IN_FUNCTION) != 0
|
||||
}
|
||||
|
||||
pub fn in_control_flow(self) -> bool {
|
||||
(self.0 & Self::IN_CONTROL_FLOW) != 0
|
||||
}
|
||||
|
||||
pub fn in_keyframes(self) -> bool {
|
||||
(self.0 & Self::IN_KEYFRAMES) != 0
|
||||
}
|
||||
|
||||
pub fn in_at_root_rule(self) -> bool {
|
||||
(self.0 & Self::IN_AT_ROOT_RULE) != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAnd<ContextFlag> for u8 {
|
||||
type Output = Self;
|
||||
#[inline]
|
||||
fn bitand(self, rhs: ContextFlag) -> Self::Output {
|
||||
self & rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr<ContextFlag> for ContextFlags {
|
||||
type Output = Self;
|
||||
fn bitor(self, rhs: ContextFlag) -> Self::Output {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Comment {
|
||||
Silent,
|
||||
Loud(String),
|
||||
}
|
@ -1,396 +0,0 @@
|
||||
use codemap::Spanned;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
use crate::{
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
parse::{ContextFlags, Parser, Stmt},
|
||||
unit::Unit,
|
||||
utils::{read_until_closing_curly_brace, read_until_open_curly_brace},
|
||||
value::{Number, Value},
|
||||
Token,
|
||||
};
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn subparser_with_in_control_flow_flag<'c>(&'c mut self) -> Parser<'c, 'b> {
|
||||
Parser {
|
||||
toks: self.toks,
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: self.scopes,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
content: self.content,
|
||||
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_toks<'d>(self, toks: &'a mut Lexer<'d>) -> Parser<'a, 'd> {
|
||||
Parser {
|
||||
toks,
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: self.scopes,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
content: self.content,
|
||||
flags: self.flags,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_if(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let mut found_true = false;
|
||||
let mut body = Vec::new();
|
||||
|
||||
let init_cond = self.parse_value(true, &|_| false)?.node;
|
||||
|
||||
self.expect_char('{')?;
|
||||
|
||||
if self.toks.peek().is_none() {
|
||||
return Err(("expected \"}\".", self.span_before).into());
|
||||
}
|
||||
|
||||
if init_cond.is_true() {
|
||||
found_true = true;
|
||||
self.scopes.enter_new_scope();
|
||||
body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
|
||||
self.scopes.exit_scope();
|
||||
} else {
|
||||
self.throw_away_until_closing_curly_brace()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let start = self.toks.cursor();
|
||||
|
||||
if !self.consume_char_if_exists('@') || !self.scan_identifier("else", false) {
|
||||
self.toks.set_cursor(start);
|
||||
break;
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
'i' | 'I' | '\\' => {
|
||||
self.span_before = tok.pos;
|
||||
let mut ident = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
ident.node.make_ascii_lowercase();
|
||||
|
||||
if ident.node != "if" {
|
||||
return Err(("expected \"{\".", ident.span).into());
|
||||
}
|
||||
|
||||
let cond = if found_true {
|
||||
self.throw_away_until_open_curly_brace()?;
|
||||
false
|
||||
} else {
|
||||
let v = self.parse_value(true, &|_| false)?.node.is_true();
|
||||
self.expect_char('{')?;
|
||||
v
|
||||
};
|
||||
|
||||
if cond {
|
||||
found_true = true;
|
||||
self.scopes.enter_new_scope();
|
||||
body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
|
||||
self.scopes.exit_scope();
|
||||
} else {
|
||||
self.throw_away_until_closing_curly_brace()?;
|
||||
}
|
||||
self.whitespace();
|
||||
}
|
||||
'{' => {
|
||||
self.toks.next();
|
||||
if found_true {
|
||||
self.throw_away_until_closing_curly_brace()?;
|
||||
break;
|
||||
}
|
||||
|
||||
self.scopes.enter_new_scope();
|
||||
let tmp = self.subparser_with_in_control_flow_flag().parse_stmt();
|
||||
self.scopes.exit_scope();
|
||||
return tmp;
|
||||
}
|
||||
_ => {
|
||||
return Err(("expected \"{\".", tok.pos()).into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.whitespace();
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub(super) fn parse_for(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char('$')?;
|
||||
|
||||
let var = self
|
||||
.parse_identifier_no_interpolation(false)?
|
||||
.map_node(Into::into);
|
||||
|
||||
self.whitespace_or_comment();
|
||||
self.span_before = match self.toks.peek() {
|
||||
Some(tok) => tok.pos,
|
||||
None => return Err(("Expected \"from\".", var.span).into()),
|
||||
};
|
||||
if self.parse_identifier()?.node.to_ascii_lowercase() != "from" {
|
||||
return Err(("Expected \"from\".", var.span).into());
|
||||
}
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let from_val = self.parse_value(false, &|parser| match parser.toks.peek() {
|
||||
Some(Token { kind: 't', .. })
|
||||
| Some(Token { kind: 'T', .. })
|
||||
| Some(Token { kind: '\\', .. }) => {
|
||||
let start = parser.toks.cursor();
|
||||
|
||||
let mut ident = match parser.parse_identifier_no_interpolation(false) {
|
||||
Ok(s) => s,
|
||||
Err(..) => return false,
|
||||
};
|
||||
|
||||
ident.node.make_ascii_lowercase();
|
||||
|
||||
let v = matches!(ident.node.to_ascii_lowercase().as_str(), "to" | "through");
|
||||
|
||||
parser.toks.set_cursor(start);
|
||||
|
||||
v
|
||||
}
|
||||
Some(..) | None => false,
|
||||
})?;
|
||||
|
||||
let through = if self.scan_identifier("through", true) {
|
||||
1
|
||||
} else if self.scan_identifier("to", true) {
|
||||
0
|
||||
} else {
|
||||
return Err(("Expected \"to\" or \"through\".", self.span_before).into());
|
||||
};
|
||||
|
||||
let from = match from_val.node {
|
||||
Value::Dimension(Some(n), ..) => match n.to_i32() {
|
||||
Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
|
||||
return Err((format!("{} is not an int.", n.inspect()), from_val.span).into())
|
||||
}
|
||||
Some(v) => v,
|
||||
},
|
||||
Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()),
|
||||
v => {
|
||||
return Err((
|
||||
format!("{} is not a number.", v.inspect(from_val.span)?),
|
||||
from_val.span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let to_val = self.parse_value(true, &|_| false)?;
|
||||
let to = match to_val.node {
|
||||
Value::Dimension(Some(n), ..) => match n.to_i32() {
|
||||
Some(std::i32::MAX) | Some(std::i32::MIN) | None => {
|
||||
return Err((format!("{} is not an int.", n.inspect()), to_val.span).into())
|
||||
}
|
||||
Some(v) => v,
|
||||
},
|
||||
Value::Dimension(None, ..) => return Err(("NaN is not an int.", from_val.span).into()),
|
||||
v => {
|
||||
return Err((
|
||||
format!(
|
||||
"{} is not a number.",
|
||||
v.to_css_string(to_val.span, self.options.is_compressed())?
|
||||
),
|
||||
to_val.span,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
self.expect_char('{')?;
|
||||
|
||||
let body = read_until_closing_curly_brace(self.toks)?;
|
||||
|
||||
self.expect_char('}')?;
|
||||
|
||||
let (mut x, mut y);
|
||||
// we can't use an inclusive range here
|
||||
#[allow(clippy::range_plus_one)]
|
||||
let iter: &mut dyn Iterator<Item = i32> = if from < to {
|
||||
x = from..(to + through);
|
||||
&mut x
|
||||
} else {
|
||||
y = ((to - through)..(from + 1)).skip(1).rev();
|
||||
&mut y
|
||||
};
|
||||
|
||||
let mut stmts = Vec::new();
|
||||
|
||||
self.scopes.enter_new_scope();
|
||||
|
||||
for i in iter {
|
||||
self.scopes.insert_var_last(
|
||||
var.node,
|
||||
Value::Dimension(Some(Number::from(i)), Unit::None, true),
|
||||
);
|
||||
let mut these_stmts = self
|
||||
.subparser_with_in_control_flow_flag()
|
||||
.with_toks(&mut Lexer::new_ref(&body))
|
||||
.parse_stmt()?;
|
||||
if self.flags.in_function() {
|
||||
if !these_stmts.is_empty() {
|
||||
return Ok(these_stmts);
|
||||
}
|
||||
} else {
|
||||
stmts.append(&mut these_stmts);
|
||||
}
|
||||
}
|
||||
|
||||
self.scopes.exit_scope();
|
||||
|
||||
Ok(stmts)
|
||||
}
|
||||
|
||||
pub(super) fn parse_while(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
// technically not necessary to eat whitespace here, but since we
|
||||
// operate on raw tokens rather than an AST, it potentially saves a lot of
|
||||
// time in re-parsing
|
||||
self.whitespace_or_comment();
|
||||
let cond = read_until_open_curly_brace(self.toks)?;
|
||||
|
||||
if cond.is_empty() {
|
||||
return Err(("Expected expression.", self.span_before).into());
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
|
||||
let mut body = read_until_closing_curly_brace(self.toks)?;
|
||||
|
||||
body.push(match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => return Err(("expected \"}\".", self.span_before).into()),
|
||||
});
|
||||
|
||||
let mut stmts = Vec::new();
|
||||
let mut val = self.parse_value_from_vec(&cond, true)?;
|
||||
self.scopes.enter_new_scope();
|
||||
while val.node.is_true() {
|
||||
let mut these_stmts = self
|
||||
.subparser_with_in_control_flow_flag()
|
||||
.with_toks(&mut Lexer::new_ref(&body))
|
||||
.parse_stmt()?;
|
||||
if self.flags.in_function() {
|
||||
if !these_stmts.is_empty() {
|
||||
return Ok(these_stmts);
|
||||
}
|
||||
} else {
|
||||
stmts.append(&mut these_stmts);
|
||||
}
|
||||
val = self.parse_value_from_vec(&cond, true)?;
|
||||
}
|
||||
self.scopes.exit_scope();
|
||||
|
||||
Ok(stmts)
|
||||
}
|
||||
|
||||
pub(super) fn parse_each(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
let mut vars: Vec<Spanned<Identifier>> = Vec::new();
|
||||
|
||||
self.whitespace_or_comment();
|
||||
loop {
|
||||
self.expect_char('$')?;
|
||||
|
||||
vars.push(self.parse_identifier()?.map_node(Into::into));
|
||||
|
||||
self.whitespace_or_comment();
|
||||
if self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or(("expected \"$\".", vars[vars.len() - 1].span))?
|
||||
.kind
|
||||
== ','
|
||||
{
|
||||
self.toks.next();
|
||||
self.whitespace_or_comment();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let i = self.parse_identifier()?;
|
||||
if i.node.to_ascii_lowercase() != "in" {
|
||||
return Err(("Expected \"in\".", i.span).into());
|
||||
}
|
||||
self.whitespace_or_comment();
|
||||
let iter_val_toks = read_until_open_curly_brace(self.toks)?;
|
||||
let iter = self
|
||||
.parse_value_from_vec(&iter_val_toks, true)?
|
||||
.node
|
||||
.as_list();
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
let mut body = read_until_closing_curly_brace(self.toks)?;
|
||||
body.push(match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => return Err(("expected \"}\".", self.span_before).into()),
|
||||
});
|
||||
self.whitespace();
|
||||
|
||||
let mut stmts = Vec::new();
|
||||
|
||||
self.scopes.enter_new_scope();
|
||||
|
||||
for row in iter {
|
||||
if vars.len() == 1 {
|
||||
self.scopes.insert_var_last(vars[0].node, row);
|
||||
} else {
|
||||
for (var, val) in vars.iter().zip(
|
||||
row.as_list()
|
||||
.into_iter()
|
||||
.chain(std::iter::once(Value::Null).cycle()),
|
||||
) {
|
||||
self.scopes.insert_var_last(var.node, val);
|
||||
}
|
||||
}
|
||||
|
||||
let mut these_stmts = self
|
||||
.subparser_with_in_control_flow_flag()
|
||||
.with_toks(&mut Lexer::new_ref(&body))
|
||||
.parse_stmt()?;
|
||||
if self.flags.in_function() {
|
||||
if !these_stmts.is_empty() {
|
||||
return Ok(these_stmts);
|
||||
}
|
||||
} else {
|
||||
stmts.append(&mut these_stmts);
|
||||
}
|
||||
}
|
||||
|
||||
self.scopes.exit_scope();
|
||||
|
||||
Ok(stmts)
|
||||
}
|
||||
}
|
219
src/parse/css.rs
Normal file
219
src/parse/css.rs
Normal file
@ -0,0 +1,219 @@
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use codemap::{CodeMap, Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
ast::*, builtin::DISALLOWED_PLAIN_CSS_FUNCTION_NAMES, common::QuoteKind, error::SassResult,
|
||||
lexer::Lexer, ContextFlags, Options,
|
||||
};
|
||||
|
||||
use super::{value::ValueParser, BaseParser, StylesheetParser};
|
||||
|
||||
pub(crate) struct CssParser<'a> {
|
||||
pub toks: Lexer<'a>,
|
||||
// todo: likely superfluous
|
||||
pub map: &'a mut CodeMap,
|
||||
pub path: &'a Path,
|
||||
pub span_before: Span,
|
||||
pub flags: ContextFlags,
|
||||
pub options: &'a Options<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BaseParser<'a> for CssParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
|
||||
fn skip_silent_comment(&mut self) -> SassResult<()> {
|
||||
Err((
|
||||
"Silent comments aren't allowed in plain CSS.",
|
||||
self.toks.current_span(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StylesheetParser<'a> for CssParser<'a> {
|
||||
fn is_plain_css(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_indented(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn path(&mut self) -> &'a Path {
|
||||
self.path
|
||||
}
|
||||
|
||||
fn map(&mut self) -> &mut CodeMap {
|
||||
self.map
|
||||
}
|
||||
|
||||
fn options(&self) -> &Options {
|
||||
self.options
|
||||
}
|
||||
|
||||
fn flags(&mut self) -> &ContextFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
fn flags_mut(&mut self) -> &mut ContextFlags {
|
||||
&mut self.flags
|
||||
}
|
||||
|
||||
fn current_indentation(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn span_before(&self) -> Span {
|
||||
self.span_before
|
||||
}
|
||||
|
||||
const IDENTIFIER_LIKE: Option<fn(&mut Self) -> SassResult<Spanned<AstExpr>>> =
|
||||
Some(Self::parse_identifier_like);
|
||||
|
||||
fn parse_at_rule(
|
||||
&mut self,
|
||||
_child: fn(&mut Self) -> SassResult<AstStmt>,
|
||||
) -> SassResult<AstStmt> {
|
||||
let start = self.toks.cursor();
|
||||
|
||||
self.expect_char('@')?;
|
||||
let name = self.parse_interpolated_identifier()?;
|
||||
self.whitespace()?;
|
||||
|
||||
match name.as_plain() {
|
||||
Some("at-root") | Some("content") | Some("debug") | Some("each") | Some("error")
|
||||
| Some("extend") | Some("for") | Some("function") | Some("if") | Some("include")
|
||||
| Some("mixin") | Some("return") | Some("warn") | Some("while") => {
|
||||
self.almost_any_value(false)?;
|
||||
Err((
|
||||
"This at-rule isn't allowed in plain CSS.",
|
||||
self.toks.span_from(start),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
Some("import") => self.parse_css_import_rule(start),
|
||||
Some("media") => self.parse_media_rule(start),
|
||||
Some("-moz-document") => self._parse_moz_document_rule(name),
|
||||
Some("supports") => self.parse_supports_rule(),
|
||||
_ => self.unknown_at_rule(name, start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CssParser<'a> {
|
||||
pub fn new(
|
||||
toks: Lexer<'a>,
|
||||
map: &'a mut CodeMap,
|
||||
options: &'a Options<'a>,
|
||||
span_before: Span,
|
||||
file_name: &'a Path,
|
||||
) -> Self {
|
||||
CssParser {
|
||||
toks,
|
||||
map,
|
||||
path: file_name,
|
||||
span_before,
|
||||
flags: ContextFlags::empty(),
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_css_import_rule(&mut self, _start: usize) -> SassResult<AstStmt> {
|
||||
let url_start = self.toks.cursor();
|
||||
|
||||
let url = if self.toks.next_char_is('u') || self.toks.next_char_is('U') {
|
||||
self.parse_dynamic_url()?
|
||||
.span(self.toks.span_from(url_start))
|
||||
} else {
|
||||
let string = self.parse_interpolated_string()?;
|
||||
AstExpr::String(
|
||||
StringExpr(string.node.as_interpolation(true), QuoteKind::None),
|
||||
string.span,
|
||||
)
|
||||
.span(string.span)
|
||||
};
|
||||
|
||||
self.whitespace()?;
|
||||
let modifiers = self.try_import_modifiers()?;
|
||||
self.expect_statement_separator(Some("@import rule"))?;
|
||||
|
||||
Ok(AstStmt::ImportRule(AstImportRule {
|
||||
imports: vec![AstImport::Plain(AstPlainCssImport {
|
||||
url: Interpolation::new_with_expr(url),
|
||||
modifiers,
|
||||
span: self.toks.span_from(url_start),
|
||||
})],
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_identifier_like(&mut self) -> SassResult<Spanned<AstExpr>> {
|
||||
let start = self.toks.cursor();
|
||||
let identifier = self.parse_interpolated_identifier()?;
|
||||
let plain = identifier.as_plain().unwrap();
|
||||
|
||||
let lower = plain.to_ascii_lowercase();
|
||||
|
||||
if let Some(special_fn) = ValueParser::try_parse_special_function(self, &lower, start)? {
|
||||
return Ok(special_fn);
|
||||
}
|
||||
|
||||
let before_args = self.toks.cursor();
|
||||
|
||||
if !self.scan_char('(') {
|
||||
let span = self.toks.span_from(start);
|
||||
return Ok(AstExpr::String(StringExpr(identifier, QuoteKind::None), span).span(span));
|
||||
}
|
||||
|
||||
let allow_empty_second_arg = lower == "var";
|
||||
|
||||
let mut arguments = Vec::new();
|
||||
|
||||
if !self.scan_char(')') {
|
||||
loop {
|
||||
self.whitespace()?;
|
||||
|
||||
let arg_start = self.toks.cursor();
|
||||
if allow_empty_second_arg && arguments.len() == 1 && self.toks.next_char_is(')') {
|
||||
arguments.push(AstExpr::String(
|
||||
StringExpr(Interpolation::new_plain(String::new()), QuoteKind::None),
|
||||
self.toks.span_from(arg_start),
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
arguments.push(self.parse_expression_until_comma(true)?.node);
|
||||
self.whitespace()?;
|
||||
if !self.scan_char(',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.expect_char(')')?;
|
||||
}
|
||||
|
||||
let span = self.toks.span_from(start);
|
||||
|
||||
if DISALLOWED_PLAIN_CSS_FUNCTION_NAMES.contains(plain) {
|
||||
return Err(("This function isn't allowed in plain CSS.", span).into());
|
||||
}
|
||||
|
||||
Ok(AstExpr::InterpolatedFunction(InterpolatedFunction {
|
||||
name: identifier,
|
||||
arguments: Box::new(ArgumentInvocation {
|
||||
positional: arguments,
|
||||
named: BTreeMap::new(),
|
||||
rest: None,
|
||||
keyword_rest: None,
|
||||
span: self.toks.span_from(before_args),
|
||||
}),
|
||||
span,
|
||||
})
|
||||
.span(span))
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
args::CallArgs,
|
||||
atrule::Function,
|
||||
common::{unvendor, Identifier},
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
scope::Scopes,
|
||||
utils::read_until_closing_curly_brace,
|
||||
value::{SassFunction, Value},
|
||||
};
|
||||
|
||||
use super::{common::ContextFlags, Parser, Stmt};
|
||||
|
||||
/// Names that functions are not allowed to have
|
||||
const RESERVED_IDENTIFIERS: [&str; 8] = [
|
||||
"calc",
|
||||
"element",
|
||||
"expression",
|
||||
"url",
|
||||
"and",
|
||||
"or",
|
||||
"not",
|
||||
"clamp",
|
||||
];
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn parse_function(&mut self) -> SassResult<()> {
|
||||
self.whitespace_or_comment();
|
||||
let Spanned { node: name, span } = self.parse_identifier()?;
|
||||
|
||||
if self.flags.in_mixin() {
|
||||
return Err(("Mixins may not contain function declarations.", span).into());
|
||||
}
|
||||
|
||||
if self.flags.in_control_flow() {
|
||||
return Err(("Functions may not be declared in control directives.", span).into());
|
||||
}
|
||||
|
||||
if self.flags.in_function() {
|
||||
return Err(("This at-rule is not allowed here.", self.span_before).into());
|
||||
}
|
||||
|
||||
if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) {
|
||||
return Err(("Invalid function name.", span).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char('(')?;
|
||||
|
||||
let args = self.parse_func_args()?;
|
||||
|
||||
self.whitespace();
|
||||
|
||||
let mut body = read_until_closing_curly_brace(self.toks)?;
|
||||
body.push(match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => return Err(("expected \"}\".", self.span_before).into()),
|
||||
});
|
||||
self.whitespace();
|
||||
|
||||
let function = Function::new(args, body, self.at_root, span);
|
||||
|
||||
let name_as_ident = Identifier::from(name);
|
||||
|
||||
let sass_function = SassFunction::UserDefined {
|
||||
function: Box::new(function),
|
||||
name: name_as_ident,
|
||||
};
|
||||
|
||||
if self.at_root {
|
||||
self.global_scope.insert_fn(name_as_ident, sass_function);
|
||||
} else {
|
||||
self.scopes.insert_fn(name_as_ident, sass_function);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn parse_return(&mut self) -> SassResult<Box<Value>> {
|
||||
let v = self.parse_value(true, &|_| false)?;
|
||||
|
||||
self.consume_char_if_exists(';');
|
||||
|
||||
Ok(Box::new(v.node))
|
||||
}
|
||||
|
||||
pub fn eval_function(
|
||||
&mut self,
|
||||
function: Function,
|
||||
args: CallArgs,
|
||||
module: Option<Spanned<Identifier>>,
|
||||
) -> SassResult<Value> {
|
||||
let Function {
|
||||
body,
|
||||
args: fn_args,
|
||||
declared_at_root,
|
||||
..
|
||||
} = function;
|
||||
|
||||
let scope = self.eval_args(&fn_args, args)?;
|
||||
|
||||
let mut new_scope = Scopes::new();
|
||||
let mut entered_scope = false;
|
||||
if declared_at_root {
|
||||
new_scope.enter_scope(scope);
|
||||
} else {
|
||||
entered_scope = true;
|
||||
self.scopes.enter_scope(scope);
|
||||
};
|
||||
|
||||
if let Some(module) = module {
|
||||
let module = self.modules.get(module.node, module.span)?;
|
||||
|
||||
if declared_at_root {
|
||||
new_scope.enter_scope(module.scope.clone());
|
||||
} else {
|
||||
self.scopes.enter_scope(module.scope.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut return_value = Parser {
|
||||
toks: &mut Lexer::new(body),
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: if declared_at_root {
|
||||
&mut new_scope
|
||||
} else {
|
||||
self.scopes
|
||||
},
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
content: self.content,
|
||||
flags: self.flags | ContextFlags::IN_FUNCTION,
|
||||
at_root: false,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
.parse_stmt()?;
|
||||
|
||||
if entered_scope {
|
||||
self.scopes.exit_scope();
|
||||
}
|
||||
|
||||
if module.is_some() {
|
||||
self.scopes.exit_scope();
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
return_value.len() <= 1,
|
||||
"we expect there to be only one return value"
|
||||
);
|
||||
match return_value
|
||||
.pop()
|
||||
.ok_or(("Function finished without @return.", self.span_before))?
|
||||
{
|
||||
Stmt::Return(v) => Ok(*v),
|
||||
_ => todo!("should be unreachable"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
use std::{borrow::Borrow, iter::Iterator};
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
common::QuoteKind,
|
||||
error::SassResult,
|
||||
utils::{as_hex, hex_char_for, is_name, is_name_start},
|
||||
value::Value,
|
||||
Token,
|
||||
};
|
||||
|
||||
use super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn ident_body_no_interpolation(&mut self, unit: bool) -> SassResult<Spanned<String>> {
|
||||
let mut text = String::new();
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
self.span_before = self.span_before.merge(tok.pos());
|
||||
if unit && tok.kind == '-' {
|
||||
// Disallow `-` followed by a dot or a digit digit in units.
|
||||
let second = match self.toks.peek_forward(1) {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
|
||||
self.toks.peek_backward(1).unwrap();
|
||||
|
||||
if second.kind == '.' || second.kind.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
text.push('-');
|
||||
} else if is_name(tok.kind) {
|
||||
text.push(self.toks.next().unwrap().kind);
|
||||
} else if tok.kind == '\\' {
|
||||
self.toks.next();
|
||||
text.push_str(&self.parse_escape(false)?);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Spanned {
|
||||
node: text,
|
||||
span: self.span_before,
|
||||
})
|
||||
}
|
||||
|
||||
fn interpolated_ident_body(&mut self, buf: &mut String) -> SassResult<()> {
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => {
|
||||
self.span_before = self.span_before.merge(tok.pos());
|
||||
buf.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
'\\' => {
|
||||
self.toks.next();
|
||||
buf.push_str(&self.parse_escape(false)?);
|
||||
}
|
||||
'#' => {
|
||||
if let Some(Token { kind: '{', .. }) = self.toks.peek_forward(1) {
|
||||
self.toks.next();
|
||||
self.toks.next();
|
||||
// TODO: if ident, interpolate literally
|
||||
let interpolation = self.parse_interpolation()?;
|
||||
buf.push_str(
|
||||
&interpolation
|
||||
.node
|
||||
.to_css_string(interpolation.span, self.options.is_compressed())?,
|
||||
);
|
||||
} else {
|
||||
self.toks.reset_cursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_escape(&mut self, identifier_start: bool) -> SassResult<String> {
|
||||
let mut value = 0;
|
||||
let first = match self.toks.peek() {
|
||||
Some(t) => t,
|
||||
None => return Err(("Expected expression.", self.span_before).into()),
|
||||
};
|
||||
let mut span = first.pos();
|
||||
if first.kind == '\n' {
|
||||
return Err(("Expected escape sequence.", span).into());
|
||||
} else if first.kind.is_ascii_hexdigit() {
|
||||
for _ in 0..6 {
|
||||
let next = match self.toks.peek() {
|
||||
Some(t) => t,
|
||||
None => break,
|
||||
};
|
||||
if !next.kind.is_ascii_hexdigit() {
|
||||
break;
|
||||
}
|
||||
value *= 16;
|
||||
span = span.merge(next.pos);
|
||||
value += as_hex(next.kind);
|
||||
self.toks.next();
|
||||
}
|
||||
if matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token { kind: ' ', .. })
|
||||
| Some(Token { kind: '\n', .. })
|
||||
| Some(Token { kind: '\t', .. })
|
||||
) {
|
||||
self.toks.next();
|
||||
}
|
||||
} else {
|
||||
span = span.merge(first.pos);
|
||||
value = first.kind as u32;
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
let c = std::char::from_u32(value).ok_or(("Invalid Unicode code point.", span))?;
|
||||
if (identifier_start && is_name_start(c) && !c.is_ascii_digit())
|
||||
|| (!identifier_start && is_name(c))
|
||||
{
|
||||
Ok(c.to_string())
|
||||
} else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_ascii_digit()) {
|
||||
let mut buf = String::with_capacity(4);
|
||||
buf.push('\\');
|
||||
if value > 0xF {
|
||||
buf.push(hex_char_for(value >> 4));
|
||||
}
|
||||
buf.push(hex_char_for(value & 0xF));
|
||||
buf.push(' ');
|
||||
Ok(buf)
|
||||
} else {
|
||||
Ok(format!("\\{}", c))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_identifier(&mut self) -> SassResult<Spanned<String>> {
|
||||
let Token { kind, pos } = self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or(("Expected identifier.", self.span_before))?;
|
||||
let mut text = String::new();
|
||||
if kind == '-' {
|
||||
self.toks.next();
|
||||
text.push('-');
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '-', .. }) => {
|
||||
self.toks.next();
|
||||
text.push('-');
|
||||
self.interpolated_ident_body(&mut text)?;
|
||||
return Ok(Spanned {
|
||||
node: text,
|
||||
span: pos,
|
||||
});
|
||||
}
|
||||
Some(..) => {}
|
||||
None => {
|
||||
return Ok(Spanned {
|
||||
node: text,
|
||||
span: self.span_before,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Token { kind: first, pos } = match self.toks.peek() {
|
||||
Some(v) => v,
|
||||
None => return Err(("Expected identifier.", self.span_before).into()),
|
||||
};
|
||||
|
||||
match first {
|
||||
c if is_name_start(c) => {
|
||||
text.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
'\\' => {
|
||||
self.toks.next();
|
||||
text.push_str(&self.parse_escape(true)?);
|
||||
}
|
||||
'#' if matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) => {
|
||||
self.toks.next();
|
||||
self.toks.next();
|
||||
match self.parse_interpolation()?.node {
|
||||
Value::String(ref s, ..) => text.push_str(s),
|
||||
v => text.push_str(
|
||||
v.to_css_string(self.span_before, self.options.is_compressed())?
|
||||
.borrow(),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => return Err(("Expected identifier.", pos).into()),
|
||||
}
|
||||
|
||||
self.interpolated_ident_body(&mut text)?;
|
||||
Ok(Spanned {
|
||||
node: text,
|
||||
span: self.span_before,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_identifier_no_interpolation(
|
||||
&mut self,
|
||||
unit: bool,
|
||||
) -> SassResult<Spanned<String>> {
|
||||
let Token {
|
||||
kind,
|
||||
pos: mut span,
|
||||
} = self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or(("Expected identifier.", self.span_before))?;
|
||||
let mut text = String::new();
|
||||
if kind == '-' {
|
||||
self.toks.next();
|
||||
text.push('-');
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '-', .. }) => {
|
||||
self.toks.next();
|
||||
text.push('-');
|
||||
text.push_str(&self.ident_body_no_interpolation(unit)?.node);
|
||||
return Ok(Spanned { node: text, span });
|
||||
}
|
||||
Some(..) => {}
|
||||
None => return Ok(Spanned { node: text, span }),
|
||||
}
|
||||
}
|
||||
|
||||
let first = match self.toks.next() {
|
||||
Some(v) => v,
|
||||
None => return Err(("Expected identifier.", span).into()),
|
||||
};
|
||||
|
||||
if is_name_start(first.kind) {
|
||||
text.push(first.kind);
|
||||
} else if first.kind == '\\' {
|
||||
text.push_str(&self.parse_escape(true)?);
|
||||
} else {
|
||||
return Err(("Expected identifier.", first.pos).into());
|
||||
}
|
||||
|
||||
let body = self.ident_body_no_interpolation(unit)?;
|
||||
span = span.merge(body.span);
|
||||
text.push_str(&body.node);
|
||||
Ok(Spanned { node: text, span })
|
||||
}
|
||||
|
||||
pub(crate) fn parse_quoted_string(&mut self, q: char) -> SassResult<Spanned<Value>> {
|
||||
let mut s = String::new();
|
||||
let mut span = self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or((format!("Expected {}.", q), self.span_before))?
|
||||
.pos();
|
||||
while let Some(tok) = self.toks.next() {
|
||||
span = span.merge(tok.pos());
|
||||
match tok.kind {
|
||||
'"' if q == '"' => {
|
||||
return Ok(Spanned {
|
||||
node: Value::String(s, QuoteKind::Quoted),
|
||||
span,
|
||||
});
|
||||
}
|
||||
'\'' if q == '\'' => {
|
||||
return Ok(Spanned {
|
||||
node: Value::String(s, QuoteKind::Quoted),
|
||||
span,
|
||||
})
|
||||
}
|
||||
'#' => {
|
||||
if let Some(Token { kind: '{', pos }) = self.toks.peek() {
|
||||
self.span_before = self.span_before.merge(pos);
|
||||
self.toks.next();
|
||||
let interpolation = self.parse_interpolation()?;
|
||||
match interpolation.node {
|
||||
Value::String(ref v, ..) => s.push_str(v),
|
||||
v => s.push_str(
|
||||
v.to_css_string(interpolation.span, self.options.is_compressed())?
|
||||
.borrow(),
|
||||
),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
s.push('#');
|
||||
continue;
|
||||
}
|
||||
'\n' => return Err(("Expected \".", tok.pos()).into()),
|
||||
'\\' => {
|
||||
let first = match self.toks.peek() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
s.push('\u{FFFD}');
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if first.kind == '\n' {
|
||||
self.toks.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if first.kind.is_ascii_hexdigit() {
|
||||
let mut value = 0;
|
||||
for _ in 0..6 {
|
||||
let next = match self.toks.peek() {
|
||||
Some(c) => c,
|
||||
None => break,
|
||||
};
|
||||
if !next.kind.is_ascii_hexdigit() {
|
||||
break;
|
||||
}
|
||||
value = (value << 4) + as_hex(self.toks.next().unwrap().kind);
|
||||
}
|
||||
|
||||
if self.toks.peek().is_some()
|
||||
&& self.toks.peek().unwrap().kind.is_ascii_whitespace()
|
||||
{
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF
|
||||
{
|
||||
s.push('\u{FFFD}');
|
||||
} else {
|
||||
s.push(std::char::from_u32(value).unwrap());
|
||||
}
|
||||
} else {
|
||||
s.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
}
|
||||
_ => s.push(tok.kind),
|
||||
}
|
||||
}
|
||||
Err((format!("Expected {}.", q), span).into())
|
||||
}
|
||||
|
||||
/// Returns whether the scanner is immediately before a plain CSS identifier.
|
||||
///
|
||||
// todo: foward arg
|
||||
/// If `forward` is passed, this looks that many characters forward instead.
|
||||
///
|
||||
/// This is based on [the CSS algorithm][], but it assumes all backslashes
|
||||
/// start escapes.
|
||||
///
|
||||
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
|
||||
pub fn looking_at_identifier(&mut self) -> bool {
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true,
|
||||
Some(Token { kind: '-', .. }) => {}
|
||||
Some(..) | None => return false,
|
||||
}
|
||||
|
||||
match self.toks.peek_forward(1) {
|
||||
Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => {
|
||||
self.toks.reset_cursor();
|
||||
true
|
||||
}
|
||||
Some(..) | None => {
|
||||
self.toks.reset_cursor();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
use std::{ffi::OsStr, path::Path, path::PathBuf};
|
||||
|
||||
use codemap::{Span, Spanned};
|
||||
|
||||
use crate::{
|
||||
common::{ListSeparator::Comma, QuoteKind},
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
value::Value,
|
||||
Token,
|
||||
};
|
||||
|
||||
use super::{Parser, Stmt};
|
||||
|
||||
#[allow(clippy::case_sensitive_file_extension_comparisons)]
|
||||
fn is_plain_css_import(url: &str) -> bool {
|
||||
if url.len() < 5 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let lower = url.to_ascii_lowercase();
|
||||
|
||||
lower.ends_with(".css")
|
||||
|| lower.starts_with("http://")
|
||||
|| lower.starts_with("https://")
|
||||
|| lower.starts_with("//")
|
||||
}
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
/// Searches the current directory of the file then searches in `load_paths` directories
|
||||
/// if the import has not yet been found.
|
||||
///
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
|
||||
pub(super) fn find_import(&self, path: &Path) -> Option<PathBuf> {
|
||||
let path_buf = if path.is_absolute() {
|
||||
// todo: test for absolute path imports
|
||||
path.into()
|
||||
} else {
|
||||
self.path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(""))
|
||||
.join(path)
|
||||
};
|
||||
|
||||
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
|
||||
|
||||
macro_rules! try_path {
|
||||
($name:expr) => {
|
||||
let name = $name;
|
||||
if self.options.fs.is_file(&name) {
|
||||
return Some(name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try_path!(path_buf.with_file_name(name).with_extension("scss"));
|
||||
try_path!(path_buf
|
||||
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"));
|
||||
try_path!(path_buf.clone());
|
||||
try_path!(path_buf.join("index.scss"));
|
||||
try_path!(path_buf.join("_index.scss"));
|
||||
|
||||
for path in &self.options.load_paths {
|
||||
if self.options.fs.is_dir(path) {
|
||||
try_path!(path.join(name).with_extension("scss"));
|
||||
try_path!(path
|
||||
.join(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"));
|
||||
try_path!(path.join("index.scss"));
|
||||
try_path!(path.join("_index.scss"));
|
||||
} else {
|
||||
try_path!(path.to_path_buf());
|
||||
try_path!(path.with_file_name(name).with_extension("scss"));
|
||||
try_path!(path
|
||||
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"));
|
||||
try_path!(path.join("index.scss"));
|
||||
try_path!(path.join("_index.scss"));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn parse_single_import(
|
||||
&mut self,
|
||||
file_name: &str,
|
||||
span: Span,
|
||||
) -> SassResult<Vec<Stmt>> {
|
||||
let path: &Path = file_name.as_ref();
|
||||
|
||||
if let Some(name) = self.find_import(path) {
|
||||
let file = self.map.add_file(
|
||||
name.to_string_lossy().into(),
|
||||
String::from_utf8(self.options.fs.read(&name)?)?,
|
||||
);
|
||||
return Parser {
|
||||
toks: &mut Lexer::new_from_file(&file),
|
||||
map: self.map,
|
||||
path: &name,
|
||||
scopes: self.scopes,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: file.span.subspan(0, 0),
|
||||
content: self.content,
|
||||
flags: self.flags,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
.parse();
|
||||
}
|
||||
|
||||
Err(("Can't find stylesheet to import.", span).into())
|
||||
}
|
||||
|
||||
pub(super) fn import(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
if self.flags.in_function() {
|
||||
return Err(("This at-rule is not allowed here.", self.span_before).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '\'', .. })
|
||||
| Some(Token { kind: '"', .. })
|
||||
| Some(Token { kind: 'u', .. }) => {}
|
||||
Some(Token { pos, .. }) => return Err(("Expected string.", pos).into()),
|
||||
None => return Err(("expected more input.", self.span_before).into()),
|
||||
};
|
||||
let Spanned {
|
||||
node: file_name_as_value,
|
||||
span,
|
||||
} = self.parse_value(true, &|_| false)?;
|
||||
|
||||
match file_name_as_value {
|
||||
Value::String(s, QuoteKind::Quoted) => {
|
||||
if is_plain_css_import(&s) {
|
||||
Ok(vec![Stmt::Import(format!("\"{}\"", s))])
|
||||
} else {
|
||||
self.parse_single_import(&s, span)
|
||||
}
|
||||
}
|
||||
Value::String(s, QuoteKind::None) => {
|
||||
if s.starts_with("url(") {
|
||||
Ok(vec![Stmt::Import(s)])
|
||||
} else {
|
||||
self.parse_single_import(&s, span)
|
||||
}
|
||||
}
|
||||
Value::List(v, Comma, _) => {
|
||||
let mut list_of_imports: Vec<Stmt> = Vec::new();
|
||||
for file_name_element in v {
|
||||
match file_name_element {
|
||||
#[allow(clippy::case_sensitive_file_extension_comparisons)]
|
||||
Value::String(s, QuoteKind::Quoted) => {
|
||||
let lower = s.to_ascii_lowercase();
|
||||
if lower.ends_with(".css")
|
||||
|| lower.starts_with("http://")
|
||||
|| lower.starts_with("https://")
|
||||
{
|
||||
list_of_imports.push(Stmt::Import(format!("\"{}\"", s)));
|
||||
} else {
|
||||
list_of_imports.append(&mut self.parse_single_import(&s, span)?);
|
||||
}
|
||||
}
|
||||
Value::String(s, QuoteKind::None) => {
|
||||
if s.starts_with("url(") {
|
||||
list_of_imports.push(Stmt::Import(s));
|
||||
} else {
|
||||
list_of_imports.append(&mut self.parse_single_import(&s, span)?);
|
||||
}
|
||||
}
|
||||
_ => return Err(("Expected string.", span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(list_of_imports)
|
||||
}
|
||||
_ => Err(("Expected string.", span).into()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
atrule::keyframes::{Keyframes, KeyframesSelector},
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
parse::Stmt,
|
||||
Token,
|
||||
};
|
||||
use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, token::Token};
|
||||
|
||||
use super::{common::ContextFlags, Parser};
|
||||
use super::BaseParser;
|
||||
|
||||
impl fmt::Display for KeyframesSelector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@ -20,201 +14,128 @@ impl fmt::Display for KeyframesSelector {
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyframesSelectorParser<'a, 'b, 'c> {
|
||||
parser: &'a mut Parser<'b, 'c>,
|
||||
pub(crate) struct KeyframesSelectorParser<'a> {
|
||||
toks: Lexer<'a>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c> KeyframesSelectorParser<'a, 'b, 'c> {
|
||||
pub fn new(parser: &'a mut Parser<'b, 'c>) -> Self {
|
||||
Self { parser }
|
||||
impl<'a> BaseParser<'a> for KeyframesSelectorParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> {
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> KeyframesSelectorParser<'a> {
|
||||
pub fn new(toks: Lexer<'a>) -> KeyframesSelectorParser<'a> {
|
||||
KeyframesSelectorParser { toks }
|
||||
}
|
||||
|
||||
pub fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> {
|
||||
let mut selectors = Vec::new();
|
||||
self.parser.whitespace_or_comment();
|
||||
while let Some(tok) = self.parser.toks.peek() {
|
||||
match tok.kind {
|
||||
't' | 'T' => {
|
||||
let mut ident = self.parser.parse_identifier()?;
|
||||
ident.node.make_ascii_lowercase();
|
||||
if ident.node == "to" {
|
||||
loop {
|
||||
self.whitespace()?;
|
||||
if self.looking_at_identifier() {
|
||||
if self.scan_identifier("to", false)? {
|
||||
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" {
|
||||
} else if self.scan_identifier("from", false)? {
|
||||
selectors.push(KeyframesSelector::From);
|
||||
} else {
|
||||
return Err(("Expected \"to\" or \"from\".", tok.pos).into());
|
||||
return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into());
|
||||
}
|
||||
}
|
||||
'0'..='9' => {
|
||||
let mut num = self.parser.parse_whole_number();
|
||||
|
||||
if let Some(Token { kind: '.', .. }) = self.parser.toks.peek() {
|
||||
self.parser.toks.next();
|
||||
num.push('.');
|
||||
num.push_str(&self.parser.parse_whole_number());
|
||||
}
|
||||
|
||||
self.parser.expect_char('%')?;
|
||||
|
||||
selectors.push(KeyframesSelector::Percent(num.into_boxed_str()));
|
||||
}
|
||||
'{' => break,
|
||||
'\\' => todo!("escaped chars in @keyframes selector"),
|
||||
_ => return Err(("Expected \"to\" or \"from\".", tok.pos).into()),
|
||||
}
|
||||
self.parser.whitespace_or_comment();
|
||||
if let Some(Token { kind: ',', .. }) = self.parser.toks.peek() {
|
||||
self.parser.toks.next();
|
||||
self.parser.whitespace_or_comment();
|
||||
} else {
|
||||
selectors.push(self.parse_percentage_selector()?);
|
||||
}
|
||||
|
||||
self.whitespace()?;
|
||||
|
||||
if !self.scan_char(',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(selectors)
|
||||
}
|
||||
|
||||
fn parse_percentage_selector(&mut self) -> SassResult<KeyframesSelector> {
|
||||
let mut buffer = String::new();
|
||||
|
||||
if self.scan_char('+') {
|
||||
buffer.push('+');
|
||||
}
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn parse_keyframes_name(&mut self) -> SassResult<String> {
|
||||
let mut name = String::new();
|
||||
self.whitespace_or_comment();
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
'#' => {
|
||||
if self.consume_char_if_exists('{') {
|
||||
name.push_str(&self.parse_interpolation_as_string()?);
|
||||
} else {
|
||||
name.push('#');
|
||||
}
|
||||
}
|
||||
' ' | '\n' | '\t' => {
|
||||
self.whitespace();
|
||||
name.push(' ');
|
||||
}
|
||||
'{' => {
|
||||
// todo: we can avoid the reallocation by trimming before emitting
|
||||
// (in `output.rs`)
|
||||
return Ok(name.trim().to_owned());
|
||||
}
|
||||
_ => name.push(tok.kind),
|
||||
}
|
||||
}
|
||||
Err(("expected \"{\".", self.span_before).into())
|
||||
}
|
||||
|
||||
pub(super) fn parse_keyframes_selector(
|
||||
&mut self,
|
||||
mut string: String,
|
||||
) -> SassResult<Vec<KeyframesSelector>> {
|
||||
let mut span = if let Some(tok) = self.toks.peek() {
|
||||
tok.pos()
|
||||
} else {
|
||||
return Err(("expected \"{\".", self.span_before).into());
|
||||
};
|
||||
|
||||
self.span_before = span;
|
||||
|
||||
while let Some(tok) = self.toks.next() {
|
||||
span = span.merge(tok.pos());
|
||||
match tok.kind {
|
||||
'#' => {
|
||||
if self.consume_char_if_exists('{') {
|
||||
string.push_str(
|
||||
&self
|
||||
.parse_interpolation()?
|
||||
.to_css_string(span, self.options.is_compressed())?,
|
||||
);
|
||||
} else {
|
||||
string.push('#');
|
||||
}
|
||||
}
|
||||
',' => {
|
||||
while let Some(c) = string.pop() {
|
||||
if c == ' ' || c == ',' {
|
||||
continue;
|
||||
}
|
||||
string.push(c);
|
||||
string.push(',');
|
||||
break;
|
||||
}
|
||||
}
|
||||
'/' => {
|
||||
if self.toks.peek().is_none() {
|
||||
return Err(("Expected selector.", tok.pos()).into());
|
||||
}
|
||||
self.parse_comment()?;
|
||||
self.whitespace();
|
||||
string.push(' ');
|
||||
}
|
||||
'{' => {
|
||||
let sel_toks: Vec<Token> =
|
||||
string.chars().map(|x| Token::new(span, x)).collect();
|
||||
|
||||
let selector = KeyframesSelectorParser::new(&mut Parser {
|
||||
toks: &mut Lexer::new(sel_toks),
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: self.scopes,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
content: self.content,
|
||||
flags: self.flags,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '0'..='9' | '.',
|
||||
..
|
||||
})
|
||||
.parse_keyframes_selector()?;
|
||||
|
||||
return Ok(selector);
|
||||
) {
|
||||
return Err(("Expected number.", self.toks.current_span()).into());
|
||||
}
|
||||
c => string.push(c),
|
||||
|
||||
while matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '0'..='9',
|
||||
..
|
||||
})
|
||||
) {
|
||||
buffer.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
|
||||
if self.scan_char('.') {
|
||||
buffer.push('.');
|
||||
|
||||
while matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '0'..='9',
|
||||
..
|
||||
})
|
||||
) {
|
||||
buffer.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
}
|
||||
|
||||
Err(("expected \"{\".", span).into())
|
||||
if self.scan_ident_char('e', false)? {
|
||||
buffer.push('e');
|
||||
|
||||
if matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '+' | '-',
|
||||
..
|
||||
})
|
||||
) {
|
||||
buffer.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
|
||||
pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> {
|
||||
if self.flags.in_function() {
|
||||
return Err(("This at-rule is not allowed here.", self.span_before).into());
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '0'..='9',
|
||||
..
|
||||
})
|
||||
) {
|
||||
return Err(("Expected digit.", self.toks.current_span()).into());
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
.parse_stmt()?;
|
||||
|
||||
Ok(Stmt::Keyframes(Box::new(Keyframes { rule, name, body })))
|
||||
while matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token {
|
||||
kind: '0'..='9',
|
||||
..
|
||||
})
|
||||
) {
|
||||
buffer.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_char('%')?;
|
||||
|
||||
Ok(KeyframesSelector::Percent(buffer.into_boxed_str()))
|
||||
}
|
||||
}
|
||||
|
@ -1,175 +0,0 @@
|
||||
use crate::{
|
||||
error::SassResult,
|
||||
utils::is_name_start,
|
||||
{Cow, Token},
|
||||
};
|
||||
|
||||
use super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
/// Peeks to see if the `ident` is at the current position. If it is,
|
||||
/// consume the identifier
|
||||
pub fn scan_identifier(&mut self, ident: &'static str, case_insensitive: bool) -> bool {
|
||||
let start = self.toks.cursor();
|
||||
|
||||
let mut peeked_identifier = match self.parse_identifier_no_interpolation(false) {
|
||||
Ok(v) => v.node,
|
||||
Err(..) => {
|
||||
self.toks.set_cursor(start);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if case_insensitive {
|
||||
peeked_identifier.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
if peeked_identifier == ident {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.toks.set_cursor(start);
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn expression_until_comparison(&mut self) -> SassResult<Cow<'static, str>> {
|
||||
let value = self.parse_value(false, &|parser| match parser.toks.peek() {
|
||||
Some(Token { kind: '>', .. })
|
||||
| Some(Token { kind: '<', .. })
|
||||
| Some(Token { kind: ':', .. })
|
||||
| Some(Token { kind: ')', .. }) => true,
|
||||
Some(Token { kind: '=', .. }) => {
|
||||
let is_double_eq = matches!(parser.toks.peek_next(), Some(Token { kind: '=', .. }));
|
||||
parser.toks.reset_cursor();
|
||||
// if it is a double eq, then parse as normal
|
||||
//
|
||||
// otherwise, it is a single eq and we should
|
||||
// treat it as a comparison
|
||||
!is_double_eq
|
||||
}
|
||||
_ => false,
|
||||
})?;
|
||||
|
||||
value
|
||||
.node
|
||||
.unquote()
|
||||
.to_css_string(value.span, self.options.is_compressed())
|
||||
}
|
||||
|
||||
pub(super) fn parse_media_query_list(&mut self) -> SassResult<String> {
|
||||
let mut buf = String::new();
|
||||
loop {
|
||||
self.whitespace_or_comment();
|
||||
buf.push_str(&self.parse_single_media_query()?);
|
||||
if !self.consume_char_if_exists(',') {
|
||||
break;
|
||||
}
|
||||
buf.push(',');
|
||||
buf.push(' ');
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn parse_media_feature(&mut self) -> SassResult<String> {
|
||||
if self.consume_char_if_exists('#') {
|
||||
self.expect_char('{')?;
|
||||
return Ok(self.parse_interpolation_as_string()?.into_owned());
|
||||
}
|
||||
|
||||
let mut buf = String::with_capacity(2);
|
||||
self.expect_char('(')?;
|
||||
buf.push('(');
|
||||
self.whitespace_or_comment();
|
||||
|
||||
buf.push_str(&self.expression_until_comparison()?);
|
||||
|
||||
if self.consume_char_if_exists(':') {
|
||||
self.whitespace_or_comment();
|
||||
|
||||
buf.push(':');
|
||||
buf.push(' ');
|
||||
|
||||
let value = self.parse_value(false, &|parser| {
|
||||
matches!(parser.toks.peek(), Some(Token { kind: ')', .. }))
|
||||
})?;
|
||||
self.expect_char(')')?;
|
||||
|
||||
buf.push_str(
|
||||
&value
|
||||
.node
|
||||
.to_css_string(value.span, self.options.is_compressed())?,
|
||||
);
|
||||
|
||||
self.whitespace_or_comment();
|
||||
buf.push(')');
|
||||
return Ok(buf);
|
||||
}
|
||||
|
||||
let next_tok = self.toks.peek();
|
||||
let is_angle = next_tok.map_or(false, |t| t.kind == '<' || t.kind == '>');
|
||||
if is_angle || matches!(next_tok, Some(Token { kind: '=', .. })) {
|
||||
buf.push(' ');
|
||||
// todo: remove this unwrap
|
||||
buf.push(self.toks.next().unwrap().kind);
|
||||
if is_angle && self.consume_char_if_exists('=') {
|
||||
buf.push('=');
|
||||
}
|
||||
buf.push(' ');
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
buf.push_str(&self.expression_until_comparison()?);
|
||||
}
|
||||
|
||||
self.expect_char(')')?;
|
||||
self.whitespace_or_comment();
|
||||
buf.push(')');
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn parse_single_media_query(&mut self) -> SassResult<String> {
|
||||
let mut buf = String::new();
|
||||
|
||||
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
||||
buf.push_str(&self.parse_identifier()?);
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
if !is_name_start(tok.kind) {
|
||||
return Ok(buf);
|
||||
}
|
||||
}
|
||||
|
||||
buf.push(' ');
|
||||
let ident = self.parse_identifier()?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if ident.to_ascii_lowercase() == "and" {
|
||||
buf.push_str("and ");
|
||||
} else {
|
||||
buf.push_str(&ident);
|
||||
|
||||
if self.scan_identifier("and", true) {
|
||||
self.whitespace_or_comment();
|
||||
buf.push_str(" and ");
|
||||
} else {
|
||||
return Ok(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
self.whitespace_or_comment();
|
||||
buf.push_str(&self.parse_media_feature()?);
|
||||
self.whitespace_or_comment();
|
||||
if !self.scan_identifier("and", true) {
|
||||
break;
|
||||
}
|
||||
buf.push_str(" and ");
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
138
src/parse/media_query.rs
Normal file
138
src/parse/media_query.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::{ast::MediaQuery, error::SassResult, lexer::Lexer};
|
||||
|
||||
use super::BaseParser;
|
||||
|
||||
pub(crate) struct MediaQueryParser<'a> {
|
||||
pub toks: Lexer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BaseParser<'a> for MediaQueryParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MediaQueryParser<'a> {
|
||||
pub fn new(toks: Lexer<'a>) -> MediaQueryParser<'a> {
|
||||
MediaQueryParser { toks }
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> SassResult<Vec<MediaQuery>> {
|
||||
let mut queries = Vec::new();
|
||||
loop {
|
||||
self.whitespace()?;
|
||||
queries.push(self.parse_media_query()?);
|
||||
self.whitespace()?;
|
||||
|
||||
if !self.scan_char(',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if self.toks.next().is_some() {
|
||||
return Err(("expected no more input.", self.toks.current_span()).into());
|
||||
}
|
||||
|
||||
Ok(queries)
|
||||
}
|
||||
|
||||
fn parse_media_query(&mut self) -> SassResult<MediaQuery> {
|
||||
if self.toks.next_char_is('(') {
|
||||
let mut conditions = vec![self.parse_media_in_parens()?];
|
||||
self.whitespace()?;
|
||||
|
||||
let mut conjunction = true;
|
||||
|
||||
if self.scan_identifier("and", false)? {
|
||||
self.expect_whitespace()?;
|
||||
conditions.append(&mut self.parse_media_logic_sequence("and")?);
|
||||
} else if self.scan_identifier("or", false)? {
|
||||
self.expect_whitespace()?;
|
||||
conjunction = false;
|
||||
conditions.append(&mut self.parse_media_logic_sequence("or")?);
|
||||
}
|
||||
|
||||
return Ok(MediaQuery::condition(conditions, conjunction));
|
||||
}
|
||||
|
||||
let mut modifier: Option<String> = None;
|
||||
let media_type: Option<String>;
|
||||
let identifier1 = self.parse_identifier(false, false)?;
|
||||
|
||||
if identifier1.to_ascii_lowercase() == "not" {
|
||||
self.expect_whitespace()?;
|
||||
if !self.looking_at_identifier() {
|
||||
return Ok(MediaQuery::condition(
|
||||
vec![format!("(not {})", self.parse_media_in_parens()?)],
|
||||
true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.whitespace()?;
|
||||
|
||||
if !self.looking_at_identifier() {
|
||||
return Ok(MediaQuery::media_type(Some(identifier1), None, None));
|
||||
}
|
||||
|
||||
let identifier2 = self.parse_identifier(false, false)?;
|
||||
|
||||
if identifier2.to_ascii_lowercase() == "and" {
|
||||
self.expect_whitespace()?;
|
||||
media_type = Some(identifier1);
|
||||
} else {
|
||||
self.whitespace()?;
|
||||
modifier = Some(identifier1);
|
||||
media_type = Some(identifier2);
|
||||
if self.scan_identifier("and", false)? {
|
||||
// For example, "@media only screen and ..."
|
||||
self.expect_whitespace()?;
|
||||
} else {
|
||||
// For example, "@media only screen {"
|
||||
return Ok(MediaQuery::media_type(media_type, modifier, None));
|
||||
}
|
||||
}
|
||||
|
||||
// We've consumed either `IDENTIFIER "and"` or
|
||||
// `IDENTIFIER IDENTIFIER "and"`.
|
||||
|
||||
if self.scan_identifier("not", false)? {
|
||||
// For example, "@media screen and not (...) {"
|
||||
self.expect_whitespace()?;
|
||||
return Ok(MediaQuery::media_type(
|
||||
media_type,
|
||||
modifier,
|
||||
Some(vec![format!("(not {})", self.parse_media_in_parens()?)]),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(MediaQuery::media_type(
|
||||
media_type,
|
||||
modifier,
|
||||
Some(self.parse_media_logic_sequence("and")?),
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_media_in_parens(&mut self) -> SassResult<String> {
|
||||
self.expect_char('(')?;
|
||||
let result = format!("({})", self.declaration_value(false)?);
|
||||
self.expect_char(')')?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn parse_media_logic_sequence(&mut self, operator: &'static str) -> SassResult<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
loop {
|
||||
result.push(self.parse_media_in_parens()?);
|
||||
self.whitespace()?;
|
||||
if !self.scan_identifier(operator, false)? {
|
||||
return Ok(result);
|
||||
}
|
||||
self.expect_whitespace()?;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,284 +0,0 @@
|
||||
use std::mem;
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
args::{CallArgs, FuncArgs},
|
||||
atrule::mixin::{Content, Mixin, UserDefinedMixin},
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
scope::Scopes,
|
||||
utils::read_until_closing_curly_brace,
|
||||
Token,
|
||||
};
|
||||
|
||||
use super::{common::ContextFlags, Parser, Stmt};
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn parse_mixin(&mut self) -> SassResult<()> {
|
||||
self.whitespace();
|
||||
let Spanned { node: name, span } = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
if self.flags.in_mixin() {
|
||||
return Err(("Mixins may not contain mixin declarations.", span).into());
|
||||
}
|
||||
|
||||
if self.flags.in_function() {
|
||||
return Err(("This at-rule is not allowed here.", span).into());
|
||||
}
|
||||
|
||||
if self.flags.in_control_flow() {
|
||||
return Err(("Mixins may not be declared in control directives.", span).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let args = match self.toks.next() {
|
||||
Some(Token { kind: '(', .. }) => self.parse_func_args()?,
|
||||
Some(Token { kind: '{', .. }) => FuncArgs::new(),
|
||||
Some(t) => return Err(("expected \"{\".", t.pos()).into()),
|
||||
None => return Err(("expected \"{\".", span).into()),
|
||||
};
|
||||
|
||||
self.whitespace();
|
||||
|
||||
let mut body = read_until_closing_curly_brace(self.toks)?;
|
||||
body.push(match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => return Err(("expected \"}\".", self.span_before).into()),
|
||||
});
|
||||
|
||||
// todo: `@include` can only give content when `@content` is present within the body
|
||||
// if `@content` is *not* present and `@include` attempts to give a body, we throw an error
|
||||
// `Error: Mixin doesn't accept a content block.`
|
||||
//
|
||||
// this is blocked on figuring out just how to check for this. presumably we could have a check
|
||||
// not when parsing initially, but rather when `@include`ing to see if an `@content` was found.
|
||||
|
||||
let mixin = Mixin::new_user_defined(args, body, false, self.at_root);
|
||||
|
||||
if self.at_root {
|
||||
self.global_scope.insert_mixin(name, mixin);
|
||||
} else {
|
||||
self.scopes.insert_mixin(name.into(), mixin);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn parse_include(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
if self.flags.in_function() {
|
||||
return Err(("This at-rule is not allowed here.", self.span_before).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
let name = self.parse_identifier()?.map_node(Into::into);
|
||||
|
||||
let (mixin, module) = if self.consume_char_if_exists('.') {
|
||||
let module = name;
|
||||
let name = self.parse_identifier()?.map_node(Into::into);
|
||||
|
||||
(
|
||||
self.modules
|
||||
.get(module.node, module.span)?
|
||||
.get_mixin(name)?,
|
||||
Some(module),
|
||||
)
|
||||
} else {
|
||||
(self.scopes.get_mixin(name, self.global_scope)?, None)
|
||||
};
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let args = if self.consume_char_if_exists('(') {
|
||||
self.parse_call_args()?
|
||||
} else {
|
||||
CallArgs::new(name.span)
|
||||
};
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let content_args = if let Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) =
|
||||
self.toks.peek()
|
||||
{
|
||||
let mut ident = self.parse_identifier_no_interpolation(false)?;
|
||||
ident.node.make_ascii_lowercase();
|
||||
if ident.node == "using" {
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char('(')?;
|
||||
|
||||
Some(self.parse_func_args()?)
|
||||
} else {
|
||||
return Err(("expected keyword \"using\".", ident.span).into());
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let content = if content_args.is_some()
|
||||
|| matches!(self.toks.peek(), Some(Token { kind: '{', .. }))
|
||||
{
|
||||
self.consume_char_if_exists('{');
|
||||
|
||||
let mut toks = read_until_closing_curly_brace(self.toks)?;
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
toks.push(tok);
|
||||
self.toks.next();
|
||||
}
|
||||
Some(toks)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.consume_char_if_exists(';');
|
||||
|
||||
let UserDefinedMixin {
|
||||
body,
|
||||
args: fn_args,
|
||||
declared_at_root,
|
||||
..
|
||||
} = match mixin {
|
||||
Mixin::UserDefined(u) => u,
|
||||
Mixin::Builtin(b) => {
|
||||
return b(args, self);
|
||||
}
|
||||
};
|
||||
|
||||
let scope = self.eval_args(&fn_args, args)?;
|
||||
|
||||
let scope_len = self.scopes.len();
|
||||
|
||||
if declared_at_root {
|
||||
mem::swap(self.scopes, self.content_scopes);
|
||||
}
|
||||
|
||||
self.scopes.enter_scope(scope);
|
||||
|
||||
if let Some(module) = module {
|
||||
let module = self.modules.get(module.node, module.span)?;
|
||||
self.scopes.enter_scope(module.scope.clone());
|
||||
}
|
||||
|
||||
self.content.push(Content {
|
||||
content,
|
||||
content_args,
|
||||
scope_len,
|
||||
declared_at_root,
|
||||
});
|
||||
|
||||
let body = Parser {
|
||||
toks: &mut Lexer::new(body),
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: self.scopes,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
flags: self.flags | ContextFlags::IN_MIXIN,
|
||||
content: self.content,
|
||||
at_root: false,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
.parse_stmt()?;
|
||||
|
||||
self.content.pop();
|
||||
|
||||
if module.is_some() {
|
||||
self.scopes.exit_scope();
|
||||
}
|
||||
|
||||
self.scopes.exit_scope();
|
||||
|
||||
if declared_at_root {
|
||||
mem::swap(self.scopes, self.content_scopes);
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub(super) fn parse_content_rule(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
if !self.flags.in_mixin() {
|
||||
return Err((
|
||||
"@content is only allowed within mixin declarations.",
|
||||
self.span_before,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(if let Some(content) = self.content.pop() {
|
||||
let (mut scope_at_decl, mixin_scope) = if content.declared_at_root {
|
||||
(mem::take(self.content_scopes), Scopes::new())
|
||||
} else {
|
||||
mem::take(self.scopes).split_off(content.scope_len)
|
||||
};
|
||||
|
||||
let mut entered_scope = false;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let call_args = if self.consume_char_if_exists('(') {
|
||||
self.parse_call_args()?
|
||||
} else {
|
||||
CallArgs::new(self.span_before)
|
||||
};
|
||||
|
||||
if let Some(ref content_args) = content.content_args {
|
||||
call_args.max_args(content_args.len())?;
|
||||
|
||||
let scope = self.eval_args(content_args, call_args)?;
|
||||
scope_at_decl.enter_scope(scope);
|
||||
entered_scope = true;
|
||||
} else {
|
||||
call_args.max_args(0)?;
|
||||
}
|
||||
|
||||
let stmts = if let Some(body) = &content.content {
|
||||
Parser {
|
||||
toks: &mut Lexer::new_ref(body),
|
||||
map: self.map,
|
||||
path: self.path,
|
||||
scopes: &mut scope_at_decl,
|
||||
global_scope: self.global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: self.span_before,
|
||||
flags: self.flags,
|
||||
content: self.content,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
module_config: self.module_config,
|
||||
}
|
||||
.parse_stmt()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if entered_scope {
|
||||
scope_at_decl.exit_scope();
|
||||
}
|
||||
|
||||
scope_at_decl.merge(mixin_scope);
|
||||
|
||||
if content.declared_at_root {
|
||||
*self.content_scopes = scope_at_decl;
|
||||
} else {
|
||||
*self.scopes = scope_at_decl;
|
||||
}
|
||||
|
||||
self.content.push(content);
|
||||
|
||||
stmts
|
||||
} else {
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
}
|
1013
src/parse/mod.rs
1013
src/parse/mod.rs
File diff suppressed because it is too large
Load Diff
@ -1,305 +0,0 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
atrule::AtRuleKind,
|
||||
builtin::modules::{
|
||||
declare_module_color, declare_module_list, declare_module_map, declare_module_math,
|
||||
declare_module_meta, declare_module_selector, declare_module_string, Module, ModuleConfig,
|
||||
Modules,
|
||||
},
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
parse::{common::Comment, Parser, Stmt, VariableValue},
|
||||
scope::Scope,
|
||||
Token,
|
||||
};
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn parse_module_alias(&mut self) -> SassResult<Option<String>> {
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. })
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut ident = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
ident.node.make_ascii_lowercase();
|
||||
|
||||
if ident.node != "as" {
|
||||
return Err(("expected \";\".", ident.span).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
if self.consume_char_if_exists('*') {
|
||||
return Ok(Some('*'.to_string()));
|
||||
}
|
||||
|
||||
let name = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
Ok(Some(name.node))
|
||||
}
|
||||
|
||||
fn parse_module_config(&mut self) -> SassResult<ModuleConfig> {
|
||||
let mut config = ModuleConfig::default();
|
||||
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. })
|
||||
) {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let mut ident = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
ident.node.make_ascii_lowercase();
|
||||
if ident.node != "with" {
|
||||
return Err(("expected \";\".", ident.span).into());
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
self.span_before = ident.span;
|
||||
|
||||
self.expect_char('(')?;
|
||||
|
||||
loop {
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char('$')?;
|
||||
|
||||
let name = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char(':')?;
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let value = self.parse_value(false, &|parser| {
|
||||
matches!(
|
||||
parser.toks.peek(),
|
||||
Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. })
|
||||
)
|
||||
})?;
|
||||
|
||||
config.insert(name.map_node(Into::into), value)?;
|
||||
|
||||
match self.toks.next() {
|
||||
Some(Token { kind: ',', .. }) => {
|
||||
continue;
|
||||
}
|
||||
Some(Token { kind: ')', .. }) => {
|
||||
break;
|
||||
}
|
||||
Some(..) | None => {
|
||||
return Err(("expected \")\".", self.span_before).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load_module(
|
||||
&mut self,
|
||||
name: &str,
|
||||
config: &mut ModuleConfig,
|
||||
) -> SassResult<(Module, Vec<Stmt>)> {
|
||||
Ok(match name {
|
||||
"sass:color" => (declare_module_color(), Vec::new()),
|
||||
"sass:list" => (declare_module_list(), Vec::new()),
|
||||
"sass:map" => (declare_module_map(), Vec::new()),
|
||||
"sass:math" => (declare_module_math(), Vec::new()),
|
||||
"sass:meta" => (declare_module_meta(), Vec::new()),
|
||||
"sass:selector" => (declare_module_selector(), Vec::new()),
|
||||
"sass:string" => (declare_module_string(), Vec::new()),
|
||||
_ => {
|
||||
if let Some(import) = self.find_import(name.as_ref()) {
|
||||
let mut global_scope = Scope::new();
|
||||
|
||||
let file = self.map.add_file(
|
||||
name.to_owned(),
|
||||
String::from_utf8(self.options.fs.read(&import)?)?,
|
||||
);
|
||||
|
||||
let mut modules = Modules::default();
|
||||
|
||||
let stmts = Parser {
|
||||
toks: &mut Lexer::new_from_file(&file),
|
||||
map: self.map,
|
||||
path: &import,
|
||||
scopes: self.scopes,
|
||||
global_scope: &mut global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: file.span.subspan(0, 0),
|
||||
content: self.content,
|
||||
flags: self.flags,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: &mut modules,
|
||||
module_config: config,
|
||||
}
|
||||
.parse()?;
|
||||
|
||||
if !config.is_empty() {
|
||||
return Err((
|
||||
"This variable was not declared with !default in the @used module.",
|
||||
self.span_before,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
(Module::new_from_scope(global_scope, modules, false), stmts)
|
||||
} else {
|
||||
return Err(("Can't find stylesheet to import.", self.span_before).into());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns any multiline comments that may have been found
|
||||
/// while loading modules
|
||||
pub(super) fn load_modules(&mut self) -> SassResult<Vec<Stmt>> {
|
||||
let mut comments = Vec::new();
|
||||
|
||||
loop {
|
||||
self.whitespace();
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '@', .. }) => {
|
||||
let start = self.toks.cursor();
|
||||
|
||||
self.toks.next();
|
||||
|
||||
if let Some(Token { kind, .. }) = self.toks.peek() {
|
||||
if !matches!(kind, 'u' | 'U' | '\\') {
|
||||
self.toks.set_cursor(start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ident = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
if AtRuleKind::try_from(&ident)? != AtRuleKind::Use {
|
||||
self.toks.set_cursor(start);
|
||||
break;
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let quote = match self.toks.next() {
|
||||
Some(Token { kind: q @ '"', .. }) | Some(Token { kind: q @ '\'', .. }) => q,
|
||||
Some(..) | None => {
|
||||
return Err(("Expected string.", self.span_before).into())
|
||||
}
|
||||
};
|
||||
|
||||
let Spanned { node: module, span } = self.parse_quoted_string(quote)?;
|
||||
let module_name = module
|
||||
.unquote()
|
||||
.to_css_string(span, self.options.is_compressed())?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let module_alias = self.parse_module_alias()?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let mut config = self.parse_module_config()?;
|
||||
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char(';')?;
|
||||
|
||||
let (module, mut stmts) =
|
||||
self.load_module(module_name.as_ref(), &mut config)?;
|
||||
|
||||
comments.append(&mut stmts);
|
||||
|
||||
// if the config isn't empty here, that means
|
||||
// variables were passed to a builtin module
|
||||
if !config.is_empty() {
|
||||
return Err(("Built-in modules can't be configured.", span).into());
|
||||
}
|
||||
|
||||
let module_name = match module_alias.as_deref() {
|
||||
Some("*") => {
|
||||
self.modules.merge(module.modules);
|
||||
self.global_scope.merge_module_scope(module.scope);
|
||||
continue;
|
||||
}
|
||||
Some(..) => module_alias.unwrap(),
|
||||
None => match module_name.as_ref() {
|
||||
"sass:color" => "color".to_owned(),
|
||||
"sass:list" => "list".to_owned(),
|
||||
"sass:map" => "map".to_owned(),
|
||||
"sass:math" => "math".to_owned(),
|
||||
"sass:meta" => "meta".to_owned(),
|
||||
"sass:selector" => "selector".to_owned(),
|
||||
"sass:string" => "string".to_owned(),
|
||||
_ => module_name.into_owned(),
|
||||
},
|
||||
};
|
||||
|
||||
self.modules.insert(module_name.into(), module, span)?;
|
||||
}
|
||||
Some(Token { kind: '/', .. }) => {
|
||||
self.toks.next();
|
||||
match self.parse_comment()?.node {
|
||||
Comment::Silent => continue,
|
||||
Comment::Loud(s) => comments.push(Stmt::Comment(s)),
|
||||
}
|
||||
}
|
||||
Some(Token { kind: '$', .. }) => self.parse_variable_declaration()?,
|
||||
Some(..) | None => break,
|
||||
}
|
||||
}
|
||||
|
||||
self.toks.reset_cursor();
|
||||
|
||||
Ok(comments)
|
||||
}
|
||||
|
||||
pub(super) fn parse_module_variable_redeclaration(
|
||||
&mut self,
|
||||
module: Identifier,
|
||||
) -> SassResult<()> {
|
||||
let variable = self
|
||||
.parse_identifier_no_interpolation(false)?
|
||||
.map_node(Into::into);
|
||||
|
||||
self.whitespace_or_comment();
|
||||
self.expect_char(':')?;
|
||||
|
||||
let VariableValue {
|
||||
var_value,
|
||||
global,
|
||||
default,
|
||||
} = self.parse_variable_value()?;
|
||||
|
||||
if global {
|
||||
return Err((
|
||||
"!global isn't allowed for variables in other modules.",
|
||||
variable.span,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if default {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value = var_value?;
|
||||
|
||||
self.modules
|
||||
.get_mut(module, variable.span)?
|
||||
.update_var(variable, value.node)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
566
src/parse/sass.rs
Normal file
566
src/parse/sass.rs
Normal file
@ -0,0 +1,566 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codemap::{CodeMap, Span};
|
||||
|
||||
use crate::{ast::*, error::SassResult, lexer::Lexer, token::Token, ContextFlags, Options};
|
||||
|
||||
use super::{BaseParser, StylesheetParser};
|
||||
|
||||
pub(crate) struct SassParser<'a> {
|
||||
pub toks: Lexer<'a>,
|
||||
// todo: likely superfluous
|
||||
pub map: &'a mut CodeMap,
|
||||
pub path: &'a Path,
|
||||
pub span_before: Span,
|
||||
pub flags: ContextFlags,
|
||||
pub options: &'a Options<'a>,
|
||||
pub current_indentation: usize,
|
||||
pub next_indentation: Option<usize>,
|
||||
pub spaces: Option<bool>,
|
||||
pub next_indentation_end: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> BaseParser<'a> for SassParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
|
||||
fn whitespace_without_comments(&mut self) {
|
||||
while let Some(next) = self.toks.peek() {
|
||||
if next.kind != '\t' && next.kind != ' ' {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_loud_comment(&mut self) -> SassResult<()> {
|
||||
self.expect_char('/')?;
|
||||
self.expect_char('*')?;
|
||||
|
||||
loop {
|
||||
let mut next = self.toks.next();
|
||||
match next {
|
||||
Some(Token { kind: '\n', .. }) => {
|
||||
return Err(("expected */.", self.toks.prev_span()).into())
|
||||
}
|
||||
Some(Token { kind: '*', .. }) => {}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
loop {
|
||||
next = self.toks.next();
|
||||
|
||||
if !matches!(next, Some(Token { kind: '*', .. })) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(next, Some(Token { kind: '/', .. })) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StylesheetParser<'a> for SassParser<'a> {
|
||||
fn is_plain_css(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_indented(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn path(&mut self) -> &'a Path {
|
||||
self.path
|
||||
}
|
||||
|
||||
fn map(&mut self) -> &mut CodeMap {
|
||||
self.map
|
||||
}
|
||||
|
||||
fn options(&self) -> &Options {
|
||||
self.options
|
||||
}
|
||||
|
||||
fn flags(&mut self) -> &ContextFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
fn flags_mut(&mut self) -> &mut ContextFlags {
|
||||
&mut self.flags
|
||||
}
|
||||
|
||||
fn current_indentation(&self) -> usize {
|
||||
self.current_indentation
|
||||
}
|
||||
|
||||
fn span_before(&self) -> Span {
|
||||
self.span_before
|
||||
}
|
||||
|
||||
fn parse_style_rule_selector(&mut self) -> SassResult<Interpolation> {
|
||||
let mut buffer = Interpolation::new();
|
||||
|
||||
loop {
|
||||
buffer.add_interpolation(self.almost_any_value(true)?);
|
||||
buffer.add_char('\n');
|
||||
|
||||
if !(buffer.trailing_string().trim_end().ends_with(',') && self.scan_char('\n')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> {
|
||||
if !self.at_end_of_statement() {
|
||||
self.expect_newline()?;
|
||||
}
|
||||
|
||||
if self.peek_indentation()? <= self.current_indentation {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// todo: position: _nextIndentationEnd!.position
|
||||
// todo: error message, "Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}."
|
||||
|
||||
Err(("Nothing may be indented here", self.toks.current_span()).into())
|
||||
}
|
||||
|
||||
fn at_end_of_statement(&self) -> bool {
|
||||
matches!(self.toks.peek(), Some(Token { kind: '\n', .. }) | None)
|
||||
}
|
||||
|
||||
fn looking_at_children(&mut self) -> SassResult<bool> {
|
||||
Ok(self.at_end_of_statement() && self.peek_indentation()? > self.current_indentation)
|
||||
}
|
||||
|
||||
fn scan_else(&mut self, if_indentation: usize) -> SassResult<bool> {
|
||||
if self.peek_indentation()? != if_indentation {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let start = self.toks.cursor();
|
||||
let start_indentation = self.current_indentation;
|
||||
let start_next_indentation = self.next_indentation;
|
||||
let start_next_indentation_end = self.next_indentation_end;
|
||||
|
||||
self.read_indentation()?;
|
||||
if self.scan_char('@') && self.scan_identifier("else", false)? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
self.toks.set_cursor(start);
|
||||
self.current_indentation = start_indentation;
|
||||
self.next_indentation = start_next_indentation;
|
||||
self.next_indentation_end = start_next_indentation_end;
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn parse_children(
|
||||
&mut self,
|
||||
child: fn(&mut Self) -> SassResult<AstStmt>,
|
||||
) -> SassResult<Vec<AstStmt>> {
|
||||
let mut children = Vec::new();
|
||||
self.while_indented_lower(|parser| {
|
||||
if let Some(parsed_child) = parser.parse_child(|parser| Ok(Some(child(parser)?)))? {
|
||||
children.push(parsed_child);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
fn parse_statements(
|
||||
&mut self,
|
||||
statement: fn(&mut Self) -> SassResult<Option<AstStmt>>,
|
||||
) -> SassResult<Vec<AstStmt>> {
|
||||
if self.toks.next_char_is(' ') || self.toks.next_char_is('\t') {
|
||||
return Err((
|
||||
"Indenting at the beginning of the document is illegal.",
|
||||
self.toks.current_span(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut statements = Vec::new();
|
||||
|
||||
while self.toks.peek().is_some() {
|
||||
if let Some(child) = self.parse_child(statement)? {
|
||||
statements.push(child);
|
||||
}
|
||||
|
||||
let indentation = self.read_indentation()?;
|
||||
assert_eq!(indentation, 0);
|
||||
}
|
||||
|
||||
Ok(statements)
|
||||
}
|
||||
|
||||
fn parse_silent_comment(&mut self) -> SassResult<AstStmt> {
|
||||
let start = self.toks.cursor();
|
||||
self.expect_char('/')?;
|
||||
self.expect_char('/')?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
let parent_indentation = self.current_indentation;
|
||||
|
||||
'outer: loop {
|
||||
let comment_prefix = if self.scan_char('/') { "///" } else { "//" };
|
||||
|
||||
loop {
|
||||
buffer.push_str(comment_prefix);
|
||||
// buffer.write(commentPrefix);
|
||||
|
||||
// Skip the initial characters because we're already writing the
|
||||
// slashes.
|
||||
for _ in comment_prefix.len()..(self.current_indentation - parent_indentation) {
|
||||
buffer.push(' ');
|
||||
}
|
||||
|
||||
while self.toks.peek().is_some() && !self.toks.next_char_is('\n') {
|
||||
buffer.push(self.toks.next().unwrap().kind);
|
||||
}
|
||||
|
||||
buffer.push('\n');
|
||||
|
||||
if self.peek_indentation()? < parent_indentation {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
if self.peek_indentation()? == parent_indentation {
|
||||
// Look ahead to the next line to see if it starts another comment.
|
||||
if matches!(
|
||||
self.toks.peek_n(1 + parent_indentation),
|
||||
Some(Token { kind: '/', .. })
|
||||
) && matches!(
|
||||
self.toks.peek_n(2 + parent_indentation),
|
||||
Some(Token { kind: '/', .. })
|
||||
) {
|
||||
self.read_indentation()?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
self.read_indentation()?;
|
||||
}
|
||||
|
||||
if !self.scan("//") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AstStmt::SilentComment(AstSilentComment {
|
||||
text: buffer,
|
||||
span: self.toks.span_from(start),
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_loud_comment(&mut self) -> SassResult<AstLoudComment> {
|
||||
let start = self.toks.cursor();
|
||||
self.expect_char('/')?;
|
||||
self.expect_char('*')?;
|
||||
|
||||
let mut first = true;
|
||||
|
||||
let mut buffer = Interpolation::new_plain("/*".to_owned());
|
||||
let parent_indentation = self.current_indentation;
|
||||
|
||||
loop {
|
||||
if first {
|
||||
let beginning_of_comment = self.toks.cursor();
|
||||
|
||||
self.spaces();
|
||||
|
||||
if self.toks.next_char_is('\n') {
|
||||
self.read_indentation()?;
|
||||
buffer.add_char(' ');
|
||||
} else {
|
||||
buffer.add_string(self.toks.raw_text(beginning_of_comment));
|
||||
}
|
||||
} else {
|
||||
buffer.add_string("\n * ".to_owned());
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
for _ in 3..(self.current_indentation - parent_indentation) {
|
||||
buffer.add_char(' ');
|
||||
}
|
||||
|
||||
while self.toks.peek().is_some() {
|
||||
match self.toks.peek() {
|
||||
Some(Token {
|
||||
kind: '\n' | '\r', ..
|
||||
}) => break,
|
||||
Some(Token { kind: '#', .. }) => {
|
||||
if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) {
|
||||
buffer.add_interpolation(self.parse_single_interpolation()?);
|
||||
} else {
|
||||
buffer.add_char('#');
|
||||
self.toks.next();
|
||||
}
|
||||
}
|
||||
Some(Token { kind, .. }) => {
|
||||
buffer.add_char(kind);
|
||||
self.toks.next();
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
if self.peek_indentation()? <= parent_indentation {
|
||||
break;
|
||||
}
|
||||
|
||||
// Preserve empty lines.
|
||||
while self.looking_at_double_newline() {
|
||||
self.expect_newline()?;
|
||||
buffer.add_char('\n');
|
||||
buffer.add_char(' ');
|
||||
buffer.add_char('*');
|
||||
}
|
||||
|
||||
self.read_indentation()?;
|
||||
}
|
||||
|
||||
if !buffer.trailing_string().trim_end().ends_with("*/") {
|
||||
buffer.add_string(" */".to_owned());
|
||||
}
|
||||
|
||||
Ok(AstLoudComment {
|
||||
text: buffer,
|
||||
span: self.toks.span_from(start),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SassParser<'a> {
|
||||
pub fn new(
|
||||
toks: Lexer<'a>,
|
||||
map: &'a mut CodeMap,
|
||||
options: &'a Options<'a>,
|
||||
span_before: Span,
|
||||
file_name: &'a Path,
|
||||
) -> Self {
|
||||
let mut flags = ContextFlags::empty();
|
||||
|
||||
flags.set(ContextFlags::IS_USE_ALLOWED, true);
|
||||
|
||||
SassParser {
|
||||
toks,
|
||||
map,
|
||||
path: file_name,
|
||||
span_before,
|
||||
flags,
|
||||
options,
|
||||
current_indentation: 0,
|
||||
next_indentation: None,
|
||||
next_indentation_end: None,
|
||||
spaces: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_indentation(&mut self) -> SassResult<usize> {
|
||||
if let Some(next) = self.next_indentation {
|
||||
return Ok(next);
|
||||
}
|
||||
|
||||
if self.toks.peek().is_none() {
|
||||
self.next_indentation = Some(0);
|
||||
self.next_indentation_end = Some(self.toks.cursor());
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let start = self.toks.cursor();
|
||||
|
||||
if !self.scan_char('\n') {
|
||||
return Err(("Expected newline.", self.toks.current_span()).into());
|
||||
}
|
||||
|
||||
let mut contains_tab;
|
||||
let mut contains_space;
|
||||
let mut next_indentation;
|
||||
|
||||
loop {
|
||||
contains_tab = false;
|
||||
contains_space = false;
|
||||
next_indentation = 0;
|
||||
|
||||
while let Some(next) = self.toks.peek() {
|
||||
match next.kind {
|
||||
' ' => contains_space = true,
|
||||
'\t' => contains_tab = true,
|
||||
_ => break,
|
||||
}
|
||||
|
||||
next_indentation += 1;
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
if self.toks.peek().is_none() {
|
||||
self.next_indentation = Some(0);
|
||||
self.next_indentation_end = Some(self.toks.cursor());
|
||||
self.toks.set_cursor(start);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if !self.scan_char('\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.check_indentation_consistency(contains_tab, contains_space, start)?;
|
||||
|
||||
self.next_indentation = Some(next_indentation);
|
||||
|
||||
if next_indentation > 0 {
|
||||
self.spaces.get_or_insert(contains_space);
|
||||
}
|
||||
|
||||
self.next_indentation_end = Some(self.toks.cursor());
|
||||
self.toks.set_cursor(start);
|
||||
|
||||
Ok(next_indentation)
|
||||
}
|
||||
|
||||
fn check_indentation_consistency(
|
||||
&mut self,
|
||||
contains_tab: bool,
|
||||
contains_space: bool,
|
||||
start: usize,
|
||||
) -> SassResult<()> {
|
||||
// NOTE: error message spans here start from the beginning of the line
|
||||
if contains_tab {
|
||||
if contains_space {
|
||||
return Err((
|
||||
"Tabs and spaces may not be mixed.",
|
||||
self.toks.span_from(start),
|
||||
)
|
||||
.into());
|
||||
} else if self.spaces == Some(true) {
|
||||
return Err(("Expected spaces, was tabs.", self.toks.span_from(start)).into());
|
||||
}
|
||||
} else if contains_space && self.spaces == Some(false) {
|
||||
return Err(("Expected tabs, was spaces.", self.toks.span_from(start)).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expect_newline(&mut self) -> SassResult<()> {
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: ';', .. }) => Err((
|
||||
"semicolons aren't allowed in the indented syntax.",
|
||||
self.toks.current_span(),
|
||||
)
|
||||
.into()),
|
||||
Some(Token { kind: '\r', .. }) => {
|
||||
self.toks.next();
|
||||
self.scan_char('\n');
|
||||
Ok(())
|
||||
}
|
||||
Some(Token { kind: '\n', .. }) => {
|
||||
self.toks.next();
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(("expected newline.", self.toks.current_span()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_indentation(&mut self) -> SassResult<usize> {
|
||||
self.current_indentation = match self.next_indentation {
|
||||
Some(indent) => indent,
|
||||
None => {
|
||||
let indent = self.peek_indentation()?;
|
||||
self.next_indentation = Some(indent);
|
||||
indent
|
||||
}
|
||||
};
|
||||
|
||||
self.toks.set_cursor(self.next_indentation_end.unwrap());
|
||||
self.next_indentation = None;
|
||||
self.next_indentation_end = None;
|
||||
|
||||
Ok(self.current_indentation)
|
||||
}
|
||||
|
||||
fn while_indented_lower(
|
||||
&mut self,
|
||||
mut body: impl FnMut(&mut Self) -> SassResult<()>,
|
||||
) -> SassResult<()> {
|
||||
let parent_indentation = self.current_indentation;
|
||||
let mut child_indentation = None;
|
||||
|
||||
while self.peek_indentation()? > parent_indentation {
|
||||
let indentation = self.read_indentation()?;
|
||||
let child_indent = *child_indentation.get_or_insert(indentation);
|
||||
|
||||
if child_indent != indentation {
|
||||
return Err((
|
||||
format!("Inconsistent indentation, expected {child_indent} spaces."),
|
||||
self.toks.current_span(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
body(self)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_child(
|
||||
&mut self,
|
||||
child: impl FnOnce(&mut Self) -> SassResult<Option<AstStmt>>,
|
||||
) -> SassResult<Option<AstStmt>> {
|
||||
Ok(Some(match self.toks.peek() {
|
||||
Some(Token {
|
||||
kind: '\n' | '\r', ..
|
||||
}) => return Ok(None),
|
||||
Some(Token { kind: '$', .. }) => AstStmt::VariableDecl(
|
||||
self.parse_variable_declaration_without_namespace(None, None)?,
|
||||
),
|
||||
Some(Token { kind: '/', .. }) => match self.toks.peek_n(1) {
|
||||
Some(Token { kind: '/', .. }) => self.parse_silent_comment()?,
|
||||
Some(Token { kind: '*', .. }) => AstStmt::LoudComment(self.parse_loud_comment()?),
|
||||
_ => return child(self),
|
||||
},
|
||||
_ => return child(self),
|
||||
}))
|
||||
}
|
||||
|
||||
fn looking_at_double_newline(&mut self) -> bool {
|
||||
match self.toks.peek() {
|
||||
// todo: is this branch reachable
|
||||
Some(Token { kind: '\r', .. }) => match self.toks.peek_n(1) {
|
||||
Some(Token { kind: '\n', .. }) => {
|
||||
matches!(self.toks.peek_n(2), Some(Token { kind: '\n', .. }))
|
||||
}
|
||||
Some(Token { kind: '\r', .. }) => true,
|
||||
_ => false,
|
||||
},
|
||||
Some(Token { kind: '\n', .. }) => matches!(
|
||||
self.toks.peek_n(1),
|
||||
Some(Token {
|
||||
kind: '\n' | '\r',
|
||||
..
|
||||
})
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
88
src/parse/scss.rs
Normal file
88
src/parse/scss.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codemap::{CodeMap, Span};
|
||||
|
||||
use crate::{lexer::Lexer, ContextFlags, Options};
|
||||
|
||||
use super::{BaseParser, StylesheetParser};
|
||||
|
||||
pub(crate) struct ScssParser<'a> {
|
||||
pub toks: Lexer<'a>,
|
||||
// todo: likely superfluous
|
||||
pub map: &'a mut CodeMap,
|
||||
pub path: &'a Path,
|
||||
pub span_before: Span,
|
||||
pub flags: ContextFlags,
|
||||
pub options: &'a Options<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ScssParser<'a> {
|
||||
pub fn new(
|
||||
toks: Lexer<'a>,
|
||||
map: &'a mut CodeMap,
|
||||
options: &'a Options<'a>,
|
||||
span_before: Span,
|
||||
file_name: &'a Path,
|
||||
) -> Self {
|
||||
let mut flags = ContextFlags::empty();
|
||||
|
||||
flags.set(ContextFlags::IS_USE_ALLOWED, true);
|
||||
|
||||
ScssParser {
|
||||
toks,
|
||||
map,
|
||||
path: file_name,
|
||||
span_before,
|
||||
flags,
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BaseParser<'a> for ScssParser<'a> {
|
||||
fn toks(&self) -> &Lexer<'a> {
|
||||
&self.toks
|
||||
}
|
||||
|
||||
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||
&mut self.toks
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StylesheetParser<'a> for ScssParser<'a> {
|
||||
fn is_plain_css(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_indented(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn path(&mut self) -> &'a Path {
|
||||
self.path
|
||||
}
|
||||
|
||||
fn map(&mut self) -> &mut CodeMap {
|
||||
self.map
|
||||
}
|
||||
|
||||
fn options(&self) -> &Options {
|
||||
self.options
|
||||
}
|
||||
|
||||
fn current_indentation(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn flags(&mut self) -> &ContextFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
fn flags_mut(&mut self) -> &mut ContextFlags {
|
||||
&mut self.flags
|
||||
}
|
||||
|
||||
fn span_before(&self) -> Span {
|
||||
self.span_before
|
||||
}
|
||||
}
|
@ -1,290 +0,0 @@
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
error::SassResult,
|
||||
interner::InternedString,
|
||||
style::Style,
|
||||
utils::{is_name, is_name_start},
|
||||
value::Value,
|
||||
Token,
|
||||
};
|
||||
|
||||
use super::common::SelectorOrStyle;
|
||||
|
||||
use super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn parse_style_value_when_no_space_after_semicolon(&mut self) -> Option<Vec<Token>> {
|
||||
let mut toks = Vec::new();
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
';' | '}' => {
|
||||
self.toks.reset_cursor();
|
||||
break;
|
||||
}
|
||||
'{' => {
|
||||
self.toks.reset_cursor();
|
||||
return None;
|
||||
}
|
||||
'(' => {
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
let mut scope = 0;
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
')' => {
|
||||
if scope == 0 {
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
break;
|
||||
}
|
||||
|
||||
scope -= 1;
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
}
|
||||
'(' => {
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
scope += 1;
|
||||
}
|
||||
_ => {
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
toks.push(tok);
|
||||
self.toks.peek_forward(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(toks)
|
||||
}
|
||||
|
||||
/// Determines whether the parser is looking at a style or a selector
|
||||
///
|
||||
/// When parsing the children of a style rule, property declarations,
|
||||
/// namespaced variable declarations, and nested style rules can all begin
|
||||
/// with bare identifiers. In order to know which statement type to produce,
|
||||
/// we need to disambiguate them. We use the following criteria:
|
||||
///
|
||||
/// * If the entity starts with an identifier followed by a period and a
|
||||
/// dollar sign, it's a variable declaration. This is the simplest case,
|
||||
/// because `.$` is used in and only in variable declarations.
|
||||
///
|
||||
/// * If the entity doesn't start with an identifier followed by a colon,
|
||||
/// it's a selector. There are some additional mostly-unimportant cases
|
||||
/// here to support various declaration hacks.
|
||||
///
|
||||
/// * If the colon is followed by another colon, it's a selector.
|
||||
///
|
||||
/// * Otherwise, if the colon is followed by anything other than
|
||||
/// interpolation or a character that's valid as the beginning of an
|
||||
/// identifier, it's a declaration.
|
||||
///
|
||||
/// * If the colon is followed by interpolation or a valid identifier, try
|
||||
/// parsing it as a declaration value. If this fails, backtrack and parse
|
||||
/// it as a selector.
|
||||
///
|
||||
/// * If the declaration value is valid but is followed by "{", backtrack and
|
||||
/// parse it as a selector anyway. This ensures that ".foo:bar {" is always
|
||||
/// parsed as a selector and never as a property with nested properties
|
||||
/// beneath it.
|
||||
// todo: potentially we read the property to a string already since properties
|
||||
// are more common than selectors? this seems to be annihilating our performance
|
||||
pub(super) fn is_selector_or_style(&mut self) -> SassResult<SelectorOrStyle> {
|
||||
if let Some(first_char) = self.toks.peek() {
|
||||
if first_char.kind == '#' {
|
||||
if !matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) {
|
||||
self.toks.reset_cursor();
|
||||
return Ok(SelectorOrStyle::Selector(String::new()));
|
||||
}
|
||||
self.toks.reset_cursor();
|
||||
} else if !is_name_start(first_char.kind) && first_char.kind != '-' {
|
||||
return Ok(SelectorOrStyle::Selector(String::new()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut property = self.parse_identifier()?.node;
|
||||
let whitespace_after_property = self.whitespace_or_comment();
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: ':', .. }) => {
|
||||
self.toks.next();
|
||||
if let Some(Token { kind, .. }) = self.toks.peek() {
|
||||
return Ok(match kind {
|
||||
':' => {
|
||||
if whitespace_after_property {
|
||||
property.push(' ');
|
||||
}
|
||||
property.push(':');
|
||||
SelectorOrStyle::Selector(property)
|
||||
}
|
||||
c if is_name(c) => {
|
||||
if let Some(toks) =
|
||||
self.parse_style_value_when_no_space_after_semicolon()
|
||||
{
|
||||
let len = toks.len();
|
||||
if let Ok(val) = self.parse_value_from_vec(&toks, false) {
|
||||
self.toks.take(len).for_each(drop);
|
||||
return Ok(SelectorOrStyle::Style(
|
||||
InternedString::get_or_intern(property),
|
||||
Some(Box::new(val)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if whitespace_after_property {
|
||||
property.push(' ');
|
||||
}
|
||||
property.push(':');
|
||||
return Ok(SelectorOrStyle::Selector(property));
|
||||
}
|
||||
_ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None),
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(Token { kind: '.', .. }) => {
|
||||
if matches!(self.toks.peek_next(), Some(Token { kind: '$', .. })) {
|
||||
self.toks.next();
|
||||
self.toks.next();
|
||||
return Ok(SelectorOrStyle::ModuleVariableRedeclaration(
|
||||
property.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if whitespace_after_property {
|
||||
property.push(' ');
|
||||
}
|
||||
return Ok(SelectorOrStyle::Selector(property));
|
||||
}
|
||||
_ => {
|
||||
if whitespace_after_property {
|
||||
property.push(' ');
|
||||
}
|
||||
return Ok(SelectorOrStyle::Selector(property));
|
||||
}
|
||||
}
|
||||
Err(("expected \"{\".", self.span_before).into())
|
||||
}
|
||||
|
||||
fn parse_property(&mut self, mut super_property: String) -> SassResult<String> {
|
||||
let property = self.parse_identifier()?;
|
||||
self.whitespace_or_comment();
|
||||
// todo: expect_char(':')?;
|
||||
if self.consume_char_if_exists(':') {
|
||||
self.whitespace_or_comment();
|
||||
} else {
|
||||
return Err(("Expected \":\".", property.span).into());
|
||||
}
|
||||
|
||||
if super_property.is_empty() {
|
||||
Ok(property.node)
|
||||
} else {
|
||||
super_property.reserve(1 + property.node.len());
|
||||
super_property.push('-');
|
||||
super_property.push_str(&property.node);
|
||||
Ok(super_property)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_style_value(&mut self) -> SassResult<Spanned<Value>> {
|
||||
self.parse_value(false, &|_| false)
|
||||
}
|
||||
|
||||
pub(super) fn parse_style_group(
|
||||
&mut self,
|
||||
super_property: InternedString,
|
||||
) -> SassResult<Vec<Style>> {
|
||||
let mut styles = Vec::new();
|
||||
self.whitespace();
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
'{' => {
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
loop {
|
||||
let property = InternedString::get_or_intern(
|
||||
self.parse_property(super_property.resolve())?,
|
||||
);
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
if tok.kind == '{' {
|
||||
styles.append(&mut self.parse_style_group(property)?);
|
||||
self.whitespace();
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
if tok.kind == '}' {
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
return Ok(styles);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let value = Box::new(self.parse_style_value()?);
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '}', .. }) => {
|
||||
styles.push(Style { property, value });
|
||||
}
|
||||
Some(Token { kind: ';', .. }) => {
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
styles.push(Style { property, value });
|
||||
}
|
||||
Some(Token { kind: '{', .. }) => {
|
||||
styles.push(Style { property, value });
|
||||
styles.append(&mut self.parse_style_group(property)?);
|
||||
}
|
||||
Some(..) | None => {
|
||||
self.whitespace();
|
||||
styles.push(Style { property, value });
|
||||
}
|
||||
}
|
||||
if let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
'}' => {
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
return Ok(styles);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let value = self.parse_style_value()?;
|
||||
let t = self
|
||||
.toks
|
||||
.peek()
|
||||
.ok_or(("expected more input.", value.span))?;
|
||||
match t.kind {
|
||||
';' => {
|
||||
self.toks.next();
|
||||
self.whitespace();
|
||||
}
|
||||
'{' => {
|
||||
let mut v = vec![Style {
|
||||
property: super_property,
|
||||
value: Box::new(value),
|
||||
}];
|
||||
v.append(&mut self.parse_style_group(super_property)?);
|
||||
return Ok(v);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Ok(vec![Style {
|
||||
property: super_property,
|
||||
value: Box::new(value),
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(styles)
|
||||
}
|
||||
}
|
3058
src/parse/stylesheet.rs
Normal file
3058
src/parse/stylesheet.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,129 +0,0 @@
|
||||
//! Consume tokens without allocating
|
||||
|
||||
use crate::{error::SassResult, Token};
|
||||
|
||||
use super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn throw_away_until_newline(&mut self) {
|
||||
for tok in &mut self.toks {
|
||||
if tok.kind == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn throw_away_quoted_string(&mut self, q: char) -> SassResult<()> {
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
'"' if q == '"' => {
|
||||
return Ok(());
|
||||
}
|
||||
'\'' if q == '\'' => {
|
||||
return Ok(());
|
||||
}
|
||||
'\\' => {
|
||||
if self.toks.next().is_none() {
|
||||
return Err((format!("Expected {}.", q), tok.pos).into());
|
||||
}
|
||||
}
|
||||
'#' => match self.toks.peek() {
|
||||
Some(Token { kind: '{', .. }) => {
|
||||
self.toks.next();
|
||||
self.throw_away_until_closing_curly_brace()?;
|
||||
}
|
||||
Some(..) => {}
|
||||
None => return Err(("expected \"{\".", self.span_before).into()),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err((format!("Expected {}.", q), self.span_before).into())
|
||||
}
|
||||
|
||||
pub(super) fn throw_away_until_open_curly_brace(&mut self) -> SassResult<()> {
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
'{' => return Ok(()),
|
||||
'/' => {
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: '/', .. }) => self.throw_away_until_newline(),
|
||||
_ => {}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
'\\' | '#' => {
|
||||
self.toks.next();
|
||||
}
|
||||
q @ '"' | q @ '\'' => {
|
||||
self.throw_away_quoted_string(q)?;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(("expected \"{\".", self.span_before).into())
|
||||
}
|
||||
|
||||
pub(super) fn throw_away_until_closing_curly_brace(&mut self) -> SassResult<()> {
|
||||
let mut nesting = 0;
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
q @ '"' | q @ '\'' => {
|
||||
self.throw_away_quoted_string(q)?;
|
||||
}
|
||||
'{' => {
|
||||
nesting += 1;
|
||||
}
|
||||
'}' => {
|
||||
if nesting == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
nesting -= 1;
|
||||
}
|
||||
'/' => match self.toks.peek() {
|
||||
Some(Token { kind: '/', .. }) => {
|
||||
self.throw_away_until_newline();
|
||||
}
|
||||
Some(..) | None => continue,
|
||||
},
|
||||
'(' => {
|
||||
self.throw_away_until_closing_paren()?;
|
||||
}
|
||||
'\\' => {
|
||||
self.toks.next();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(("expected \"}\".", self.span_before).into())
|
||||
}
|
||||
|
||||
pub(super) fn throw_away_until_closing_paren(&mut self) -> SassResult<()> {
|
||||
let mut scope = 0;
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
')' => {
|
||||
if scope < 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
scope -= 1;
|
||||
}
|
||||
'(' => scope += 1,
|
||||
'"' | '\'' => {
|
||||
self.throw_away_quoted_string(tok.kind)?;
|
||||
}
|
||||
'\\' => {
|
||||
match self.toks.next() {
|
||||
Some(tok) => tok,
|
||||
None => continue,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(("expected \")\".", self.span_before).into())
|
||||
}
|
||||
}
|
1826
src/parse/value.rs
Normal file
1826
src/parse/value.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,466 +0,0 @@
|
||||
use std::{borrow::Borrow, iter::Iterator};
|
||||
|
||||
use crate::{error::SassResult, parse::common::Comment, utils::IsWhitespace, value::Value, Token};
|
||||
|
||||
use super::super::Parser;
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn parse_calc_args(&mut self, buf: &mut String) -> SassResult<()> {
|
||||
buf.reserve(2);
|
||||
buf.push('(');
|
||||
let mut nesting = 0;
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
' ' | '\t' | '\n' => {
|
||||
self.whitespace();
|
||||
buf.push(' ');
|
||||
}
|
||||
'#' => {
|
||||
if let Some(Token { kind: '{', pos }) = self.toks.peek() {
|
||||
self.span_before = pos;
|
||||
self.toks.next();
|
||||
let interpolation = self.parse_interpolation()?;
|
||||
buf.push_str(
|
||||
&interpolation
|
||||
.node
|
||||
.to_css_string(interpolation.span, self.options.is_compressed())?,
|
||||
);
|
||||
} else {
|
||||
buf.push('#');
|
||||
}
|
||||
}
|
||||
'(' => {
|
||||
nesting += 1;
|
||||
buf.push('(');
|
||||
}
|
||||
')' => {
|
||||
if nesting == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
nesting -= 1;
|
||||
buf.push(')');
|
||||
}
|
||||
q @ '\'' | q @ '"' => {
|
||||
buf.push('"');
|
||||
match self.parse_quoted_string(q)?.node {
|
||||
Value::String(ref s, ..) => buf.push_str(s),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
buf.push('"');
|
||||
}
|
||||
c => buf.push(c),
|
||||
}
|
||||
}
|
||||
buf.push(')');
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn parse_progid(&mut self) -> SassResult<String> {
|
||||
let mut string = String::new();
|
||||
let mut span = match self.toks.peek() {
|
||||
Some(token) => token.pos(),
|
||||
None => {
|
||||
return Err(("expected \"(\".", self.span_before).into());
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(tok) = self.toks.next() {
|
||||
span = span.merge(tok.pos());
|
||||
match tok.kind {
|
||||
'a'..='z' | 'A'..='Z' | '.' => {
|
||||
string.push(tok.kind);
|
||||
}
|
||||
'(' => {
|
||||
self.parse_calc_args(&mut string)?;
|
||||
break;
|
||||
}
|
||||
_ => return Err(("expected \"(\".", span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
pub(super) fn try_parse_url(&mut self) -> SassResult<Option<String>> {
|
||||
let mut buf = String::from("url(");
|
||||
|
||||
let start = self.toks.cursor();
|
||||
|
||||
self.whitespace();
|
||||
|
||||
while let Some(tok) = self.toks.next() {
|
||||
match tok.kind {
|
||||
'!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => buf.push(tok.kind),
|
||||
'#' => {
|
||||
if self.consume_char_if_exists('{') {
|
||||
let interpolation = self.parse_interpolation()?;
|
||||
match interpolation.node {
|
||||
Value::String(ref s, ..) => buf.push_str(s),
|
||||
v => buf.push_str(
|
||||
v.to_css_string(interpolation.span, self.options.is_compressed())?
|
||||
.borrow(),
|
||||
),
|
||||
};
|
||||
} else {
|
||||
buf.push('#');
|
||||
}
|
||||
}
|
||||
')' => {
|
||||
buf.push(')');
|
||||
|
||||
return Ok(Some(buf));
|
||||
}
|
||||
' ' | '\t' | '\n' | '\r' => {
|
||||
self.whitespace();
|
||||
|
||||
if self.consume_char_if_exists(')') {
|
||||
buf.push(')');
|
||||
|
||||
return Ok(Some(buf));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
self.toks.set_cursor(start);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(super) fn try_parse_min_max(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
allow_comma: bool,
|
||||
) -> SassResult<Option<String>> {
|
||||
let mut buf = if allow_comma {
|
||||
format!("{}(", fn_name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
let kind = tok.kind;
|
||||
match kind {
|
||||
'+' | '-' | '0'..='9' => {
|
||||
let number = self.parse_dimension(&|_| false)?;
|
||||
buf.push_str(
|
||||
&number
|
||||
.node
|
||||
.to_css_string(number.span, self.options.is_compressed())?,
|
||||
);
|
||||
}
|
||||
'#' => {
|
||||
self.toks.next();
|
||||
if self.consume_char_if_exists('{') {
|
||||
let interpolation = self.parse_interpolation_as_string()?;
|
||||
|
||||
buf.push_str(&interpolation);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
'c' | 'C' => {
|
||||
if let Some(name) = self.try_parse_min_max_function("calc")? {
|
||||
buf.push_str(&name);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
'e' | 'E' => {
|
||||
if let Some(name) = self.try_parse_min_max_function("env")? {
|
||||
buf.push_str(&name);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
'v' | 'V' => {
|
||||
if let Some(name) = self.try_parse_min_max_function("var")? {
|
||||
buf.push_str(&name);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
'(' => {
|
||||
self.toks.next();
|
||||
buf.push('(');
|
||||
if let Some(val) = self.try_parse_min_max(fn_name, false)? {
|
||||
buf.push_str(&val);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
'm' | 'M' => {
|
||||
self.toks.next();
|
||||
let inner_fn_name = match self.toks.peek() {
|
||||
Some(Token { kind: 'i', .. }) | Some(Token { kind: 'I', .. }) => {
|
||||
self.toks.next();
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token { kind: 'n', .. }) | Some(Token { kind: 'N', .. })
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
"min"
|
||||
}
|
||||
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => {
|
||||
self.toks.next();
|
||||
if !matches!(
|
||||
self.toks.peek(),
|
||||
Some(Token { kind: 'x', .. }) | Some(Token { kind: 'X', .. })
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
"max"
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
self.toks.next();
|
||||
|
||||
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
|
||||
if let Some(val) = self.try_parse_min_max(inner_fn_name, true)? {
|
||||
buf.push_str(&val);
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
|
||||
let next = match self.toks.peek() {
|
||||
Some(tok) => tok,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
match next.kind {
|
||||
')' => {
|
||||
self.toks.next();
|
||||
buf.push(')');
|
||||
return Ok(Some(buf));
|
||||
}
|
||||
'+' | '-' | '*' | '/' => {
|
||||
self.toks.next();
|
||||
buf.push(' ');
|
||||
buf.push(next.kind);
|
||||
buf.push(' ');
|
||||
}
|
||||
',' => {
|
||||
if !allow_comma {
|
||||
return Ok(None);
|
||||
}
|
||||
self.toks.next();
|
||||
buf.push(',');
|
||||
buf.push(' ');
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
}
|
||||
|
||||
Ok(Some(buf))
|
||||
}
|
||||
|
||||
fn try_parse_min_max_function(&mut self, fn_name: &'static str) -> SassResult<Option<String>> {
|
||||
let mut ident = self.parse_identifier_no_interpolation(false)?.node;
|
||||
ident.make_ascii_lowercase();
|
||||
|
||||
if ident != fn_name {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
ident.push('(');
|
||||
|
||||
let value = self.declaration_value(true, false, true)?;
|
||||
|
||||
if !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
|
||||
ident.push_str(&value);
|
||||
|
||||
ident.push(')');
|
||||
|
||||
Ok(Some(ident))
|
||||
}
|
||||
|
||||
pub(crate) fn declaration_value(
|
||||
&mut self,
|
||||
allow_empty: bool,
|
||||
allow_semicolon: bool,
|
||||
allow_colon: bool,
|
||||
) -> SassResult<String> {
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut brackets = Vec::new();
|
||||
let mut wrote_newline = false;
|
||||
|
||||
while let Some(tok) = self.toks.peek() {
|
||||
match tok.kind {
|
||||
'\\' => {
|
||||
self.toks.next();
|
||||
buffer.push_str(&self.parse_escape(true)?);
|
||||
wrote_newline = false;
|
||||
}
|
||||
q @ ('"' | '\'') => {
|
||||
self.toks.next();
|
||||
let s = self.parse_quoted_string(q)?;
|
||||
buffer.push_str(&s.node.to_css_string(s.span, self.options.is_compressed())?);
|
||||
wrote_newline = false;
|
||||
}
|
||||
'/' => {
|
||||
if matches!(self.toks.peek_n(1), Some(Token { kind: '*', .. })) {
|
||||
self.toks.next();
|
||||
|
||||
let comment = match self.parse_comment()?.node {
|
||||
Comment::Loud(s) => s,
|
||||
Comment::Silent => continue,
|
||||
};
|
||||
|
||||
buffer.push_str("/*");
|
||||
buffer.push_str(&comment);
|
||||
buffer.push_str("*/");
|
||||
} else {
|
||||
buffer.push('/');
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
'#' => {
|
||||
if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) {
|
||||
let s = self.parse_identifier()?;
|
||||
buffer.push_str(&s.node);
|
||||
} else {
|
||||
buffer.push('#');
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
c @ (' ' | '\t') => {
|
||||
if wrote_newline
|
||||
|| !self.toks.peek_n(1).map_or(false, |tok| tok.is_whitespace())
|
||||
{
|
||||
buffer.push(c);
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
}
|
||||
'\n' | '\r' => {
|
||||
if !wrote_newline {
|
||||
buffer.push('\n');
|
||||
}
|
||||
|
||||
wrote_newline = true;
|
||||
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
'[' | '(' | '{' => {
|
||||
buffer.push(tok.kind);
|
||||
|
||||
self.toks.next();
|
||||
|
||||
match tok.kind {
|
||||
'[' => brackets.push(']'),
|
||||
'(' => brackets.push(')'),
|
||||
'{' => brackets.push('}'),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
']' | ')' | '}' => {
|
||||
if let Some(end) = brackets.pop() {
|
||||
self.expect_char(end)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
';' => {
|
||||
if !allow_semicolon && brackets.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
buffer.push(';');
|
||||
wrote_newline = false;
|
||||
}
|
||||
':' => {
|
||||
if !allow_colon && brackets.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.toks.next();
|
||||
buffer.push(':');
|
||||
wrote_newline = false;
|
||||
}
|
||||
'u' | 'U' => {
|
||||
let before_url = self.toks.cursor();
|
||||
|
||||
if !self.scan_identifier("url", true) {
|
||||
buffer.push(tok.kind);
|
||||
self.toks.next();
|
||||
wrote_newline = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(contents) = self.try_parse_url()? {
|
||||
buffer.push_str(&contents);
|
||||
} else {
|
||||
self.toks.set_cursor(before_url);
|
||||
buffer.push(tok.kind);
|
||||
self.toks.next();
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
c => {
|
||||
if self.looking_at_identifier() {
|
||||
buffer.push_str(&self.parse_identifier()?.node);
|
||||
} else {
|
||||
self.toks.next();
|
||||
buffer.push(c);
|
||||
}
|
||||
|
||||
wrote_newline = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last) = brackets.pop() {
|
||||
self.expect_char(last)?;
|
||||
}
|
||||
|
||||
if !allow_empty && buffer.is_empty() {
|
||||
return Err(("Expected token.", self.span_before).into());
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +0,0 @@
|
||||
pub(crate) use eval::{HigherIntermediateValue, ValueVisitor};
|
||||
|
||||
mod css_function;
|
||||
mod eval;
|
||||
mod parse;
|
File diff suppressed because it is too large
Load Diff
@ -1,162 +0,0 @@
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{common::Identifier, error::SassResult, value::Value, Token};
|
||||
|
||||
use super::Parser;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct VariableValue {
|
||||
pub var_value: SassResult<Spanned<Value>>,
|
||||
pub global: bool,
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl VariableValue {
|
||||
pub const fn new(var_value: SassResult<Spanned<Value>>, global: bool, default: bool) -> Self {
|
||||
Self {
|
||||
var_value,
|
||||
global,
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
pub(super) fn parse_variable_declaration(&mut self) -> SassResult<()> {
|
||||
let next = self.toks.next();
|
||||
assert!(matches!(next, Some(Token { kind: '$', .. })));
|
||||
let ident: Identifier = self.parse_identifier_no_interpolation(false)?.node.into();
|
||||
self.whitespace_or_comment();
|
||||
|
||||
self.expect_char(':')?;
|
||||
|
||||
let VariableValue {
|
||||
var_value,
|
||||
global,
|
||||
default,
|
||||
} = self.parse_variable_value()?;
|
||||
|
||||
if default {
|
||||
let config_val = self.module_config.get(ident).filter(|v| !v.is_null());
|
||||
|
||||
let value = if (self.at_root && !self.flags.in_control_flow()) || global {
|
||||
if self.global_scope.default_var_exists(ident) {
|
||||
return Ok(());
|
||||
} else if let Some(value) = config_val {
|
||||
value
|
||||
} else {
|
||||
var_value?.node
|
||||
}
|
||||
} else if self.at_root && self.flags.in_control_flow() {
|
||||
if self.global_scope.default_var_exists(ident) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
var_value?.node
|
||||
} else {
|
||||
if self.scopes.default_var_exists(ident) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
var_value?.node
|
||||
};
|
||||
|
||||
if self.at_root && self.global_scope.var_exists(ident) {
|
||||
if !self.global_scope.default_var_exists(ident) {
|
||||
self.global_scope.insert_var(ident, value.clone());
|
||||
}
|
||||
} else if self.at_root
|
||||
&& !self.flags.in_control_flow()
|
||||
&& !self.global_scope.default_var_exists(ident)
|
||||
{
|
||||
self.global_scope.insert_var(ident, value.clone());
|
||||
}
|
||||
|
||||
if global {
|
||||
self.global_scope.insert_var(ident, value.clone());
|
||||
}
|
||||
|
||||
if self.at_root && !self.flags.in_control_flow() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.scopes.insert_var(ident, value);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value = var_value?.node;
|
||||
|
||||
if global {
|
||||
self.global_scope.insert_var(ident, value.clone());
|
||||
}
|
||||
|
||||
if self.at_root {
|
||||
if self.flags.in_control_flow() {
|
||||
if self.global_scope.var_exists(ident) {
|
||||
self.global_scope.insert_var(ident, value);
|
||||
} else {
|
||||
self.scopes.insert_var(ident, value);
|
||||
}
|
||||
} else {
|
||||
self.global_scope.insert_var(ident, value);
|
||||
}
|
||||
} else if !(self.flags.in_control_flow() && global) {
|
||||
self.scopes.insert_var(ident, value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn parse_variable_value(&mut self) -> SassResult<VariableValue> {
|
||||
let mut default = false;
|
||||
let mut global = false;
|
||||
|
||||
let value = self.parse_value(true, &|parser| {
|
||||
if matches!(parser.toks.peek(), Some(Token { kind: '!', .. })) {
|
||||
let is_important = matches!(
|
||||
parser.toks.peek_next(),
|
||||
Some(Token { kind: 'i', .. })
|
||||
| Some(Token { kind: 'I', .. })
|
||||
| Some(Token { kind: '=', .. })
|
||||
);
|
||||
parser.toks.reset_cursor();
|
||||
!is_important
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
// todo: it should not be possible to declare the same flag more than once
|
||||
while self.consume_char_if_exists('!') {
|
||||
let flag = self.parse_identifier_no_interpolation(false)?;
|
||||
|
||||
match flag.node.as_str() {
|
||||
"global" => {
|
||||
global = true;
|
||||
}
|
||||
"default" => {
|
||||
default = true;
|
||||
}
|
||||
_ => {
|
||||
return Err(("Invalid flag name.", flag.span).into());
|
||||
}
|
||||
}
|
||||
|
||||
self.whitespace_or_comment();
|
||||
}
|
||||
|
||||
match self.toks.peek() {
|
||||
Some(Token { kind: ';', .. }) => {
|
||||
self.toks.next();
|
||||
}
|
||||
Some(Token { kind: '}', .. }) => {}
|
||||
Some(..) | None => {
|
||||
value?;
|
||||
self.expect_char(';')?;
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(VariableValue::new(value, global, default))
|
||||
}
|
||||
}
|
268
src/scope.rs
268
src/scope.rs
@ -1,268 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use codemap::Spanned;
|
||||
|
||||
use crate::{
|
||||
atrule::mixin::Mixin,
|
||||
builtin::GLOBAL_FUNCTIONS,
|
||||
common::Identifier,
|
||||
error::SassResult,
|
||||
value::{SassFunction, Value},
|
||||
};
|
||||
|
||||
/// A singular scope
|
||||
///
|
||||
/// Contains variables, functions, and mixins
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct Scope {
|
||||
pub vars: BTreeMap<Identifier, Value>,
|
||||
pub mixins: BTreeMap<Identifier, Mixin>,
|
||||
pub functions: BTreeMap<Identifier, SassFunction>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
// `BTreeMap::new` is not yet const
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vars: BTreeMap::new(),
|
||||
mixins: BTreeMap::new(),
|
||||
functions: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
|
||||
match self.vars.get(&name.node) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(("Undefined variable.", name.span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_var_no_err(&self, name: Identifier) -> Option<&Value> {
|
||||
self.vars.get(&name)
|
||||
}
|
||||
|
||||
pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
|
||||
self.vars.insert(s, v)
|
||||
}
|
||||
|
||||
pub fn var_exists(&self, name: Identifier) -> bool {
|
||||
self.vars.contains_key(&name)
|
||||
}
|
||||
|
||||
fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
|
||||
match self.mixins.get(&name.node) {
|
||||
Some(v) => Ok(v.clone()),
|
||||
None => Err(("Undefined mixin.", name.span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_mixin<T: Into<Identifier>>(&mut self, s: T, v: Mixin) -> Option<Mixin> {
|
||||
self.mixins.insert(s.into(), v)
|
||||
}
|
||||
|
||||
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
||||
self.mixins.contains_key(&name)
|
||||
}
|
||||
|
||||
fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||
self.functions.get(&name).cloned()
|
||||
}
|
||||
|
||||
pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
|
||||
self.functions.insert(s, v)
|
||||
}
|
||||
|
||||
pub fn fn_exists(&self, name: Identifier) -> bool {
|
||||
if self.functions.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.functions.contains_key(&name)
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: Scope) {
|
||||
self.vars.extend(other.vars);
|
||||
self.mixins.extend(other.mixins);
|
||||
self.functions.extend(other.functions);
|
||||
}
|
||||
|
||||
pub fn merge_module_scope(&mut self, other: Scope) {
|
||||
self.merge(other);
|
||||
}
|
||||
|
||||
pub fn default_var_exists(&self, s: Identifier) -> bool {
|
||||
if let Some(default_var) = self.get_var_no_err(s) {
|
||||
!default_var.is_null()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Scopes(Vec<Scope>);
|
||||
|
||||
impl Scopes {
|
||||
pub const fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn split_off(mut self, len: usize) -> (Scopes, Scopes) {
|
||||
let split = self.0.split_off(len);
|
||||
(self, Scopes(split))
|
||||
}
|
||||
|
||||
pub fn enter_new_scope(&mut self) {
|
||||
self.0.push(Scope::new());
|
||||
}
|
||||
|
||||
pub fn enter_scope(&mut self, scope: Scope) {
|
||||
self.0.push(scope);
|
||||
}
|
||||
|
||||
pub fn exit_scope(&mut self) {
|
||||
self.0.pop();
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, mut other: Self) {
|
||||
self.0.append(&mut other.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables
|
||||
impl Scopes {
|
||||
pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
|
||||
for scope in self.0.iter_mut().rev() {
|
||||
if scope.var_exists(s) {
|
||||
return scope.insert_var(s, v);
|
||||
}
|
||||
}
|
||||
if let Some(scope) = self.0.last_mut() {
|
||||
scope.insert_var(s, v)
|
||||
} else {
|
||||
let mut scope = Scope::new();
|
||||
scope.insert_var(s, v);
|
||||
self.0.push(scope);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Always insert this variable into the innermost scope
|
||||
///
|
||||
/// Used, for example, for variables from `@each` and `@for`
|
||||
pub fn insert_var_last(&mut self, s: Identifier, v: Value) -> Option<Value> {
|
||||
if let Some(scope) = self.0.last_mut() {
|
||||
scope.insert_var(s, v)
|
||||
} else {
|
||||
let mut scope = Scope::new();
|
||||
scope.insert_var(s, v);
|
||||
self.0.push(scope);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_var_exists(&self, name: Identifier) -> bool {
|
||||
for scope in self.0.iter().rev() {
|
||||
if scope.default_var_exists(name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_var<'a>(
|
||||
&'a self,
|
||||
name: Spanned<Identifier>,
|
||||
global_scope: &'a Scope,
|
||||
) -> SassResult<&Value> {
|
||||
for scope in self.0.iter().rev() {
|
||||
if scope.var_exists(name.node) {
|
||||
return scope.get_var(name);
|
||||
}
|
||||
}
|
||||
global_scope.get_var(name)
|
||||
}
|
||||
|
||||
pub fn var_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
|
||||
for scope in &self.0 {
|
||||
if scope.var_exists(name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
global_scope.var_exists(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixins
|
||||
impl Scopes {
|
||||
pub fn insert_mixin(&mut self, s: Identifier, v: Mixin) -> Option<Mixin> {
|
||||
if let Some(scope) = self.0.last_mut() {
|
||||
scope.insert_mixin(s, v)
|
||||
} else {
|
||||
let mut scope = Scope::new();
|
||||
scope.insert_mixin(s, v);
|
||||
self.0.push(scope);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mixin<'a>(
|
||||
&'a self,
|
||||
name: Spanned<Identifier>,
|
||||
global_scope: &'a Scope,
|
||||
) -> SassResult<Mixin> {
|
||||
for scope in self.0.iter().rev() {
|
||||
if scope.mixin_exists(name.node) {
|
||||
return scope.get_mixin(name);
|
||||
}
|
||||
}
|
||||
global_scope.get_mixin(name)
|
||||
}
|
||||
|
||||
pub fn mixin_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
|
||||
for scope in &self.0 {
|
||||
if scope.mixin_exists(name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
global_scope.mixin_exists(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions
|
||||
impl Scopes {
|
||||
pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
|
||||
if let Some(scope) = self.0.last_mut() {
|
||||
scope.insert_fn(s, v)
|
||||
} else {
|
||||
let mut scope = Scope::new();
|
||||
scope.insert_fn(s, v);
|
||||
self.0.push(scope);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fn<'a>(&'a self, name: Identifier, global_scope: &'a Scope) -> Option<SassFunction> {
|
||||
for scope in self.0.iter().rev() {
|
||||
if scope.fn_exists(name) {
|
||||
return scope.get_fn(name);
|
||||
}
|
||||
}
|
||||
global_scope.get_fn(name)
|
||||
}
|
||||
|
||||
pub fn fn_exists(&self, name: Identifier, global_scope: &Scope) -> bool {
|
||||
for scope in &self.0 {
|
||||
if scope.fn_exists(name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
global_scope.fn_exists(name) || GLOBAL_FUNCTIONS.contains_key(name.as_str())
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user