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
|
name: Incorrect Sass Output
|
||||||
about: There exists a differential between the output of grass and dart-sass
|
about: `grass` and `dart-sass` differ in output or `grass` reports and error for a valid style sheet
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: connorskees
|
assignees: connorskees
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Minimal Reproducible Example**:
|
**Failing Sass**:
|
||||||
```
|
```
|
||||||
a {
|
a {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- Showing output from both tools is optional, but does help in debugging -->
|
||||||
**`grass` Output**:
|
**`grass` Output**:
|
||||||
```
|
```
|
||||||
a {
|
a {
|
||||||
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -73,10 +73,10 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build
|
||||||
|
|
||||||
- name: Install dart-sass 1.36.0
|
- name: Install dart-sass 1.54.3
|
||||||
run: |
|
run: |
|
||||||
wget https://github.com/sass/dart-sass/releases/download/1.36.0/dart-sass-1.36.0-linux-x64.tar.gz
|
wget https://github.com/sass/dart-sass/releases/download/1.54.3/dart-sass-1.54.3-linux-x64.tar.gz
|
||||||
tar -xzvf dart-sass-1.36.0-linux-x64.tar.gz
|
tar -xzvf dart-sass-1.54.3-linux-x64.tar.gz
|
||||||
|
|
||||||
- name: Install bootstrap
|
- name: Install bootstrap
|
||||||
run: git clone --depth=1 --branch v5.0.2 https://github.com/twbs/bootstrap.git
|
run: git clone --depth=1 --branch v5.0.2 https://github.com/twbs/bootstrap.git
|
||||||
|
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
|
# 0.11.2
|
||||||
|
|
||||||
- make `grass::Error` a `Send` type
|
- make `grass::Error` a `Send` type
|
||||||
|
49
Cargo.toml
49
Cargo.toml
@ -23,63 +23,32 @@ path = "src/lib.rs"
|
|||||||
# crate-type = ["cdylib", "rlib"]
|
# crate-type = ["cdylib", "rlib"]
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
path = "benches/variables.rs"
|
|
||||||
name = "variables"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
path = "benches/colors.rs"
|
|
||||||
name = "colors"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
path = "benches/numbers.rs"
|
|
||||||
name = "numbers"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
path = "benches/control_flow.rs"
|
|
||||||
name = "control_flow"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
path = "benches/styles.rs"
|
|
||||||
name = "styles"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "2.34.0", optional = true }
|
clap = { version = "2.34.0", optional = true }
|
||||||
num-rational = "0.4"
|
# todo: use lazy_static
|
||||||
num-bigint = "0.4"
|
|
||||||
num-traits = "0.2.14"
|
|
||||||
once_cell = "1.15.0"
|
once_cell = "1.15.0"
|
||||||
|
# todo: use xorshift for random numbers
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
|
# todo: update to use asref<path>
|
||||||
|
# todo: update to expose more info (for eww)
|
||||||
|
# todo: update to use text_size::TextRange
|
||||||
codemap = "0.1.3"
|
codemap = "0.1.3"
|
||||||
wasm-bindgen = { version = "0.2.68", optional = true }
|
wasm-bindgen = { version = "0.2.68", optional = true }
|
||||||
beef = "0.5"
|
# todo: use phf for global functions
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.11", features = ["macros"] }
|
||||||
# criterion is not a dev-dependency because it makes tests take too
|
indexmap = "1.9.0"
|
||||||
# long to compile, and you cannot make dev-dependencies optional
|
# todo: do we really need interning for things?
|
||||||
criterion = { version = "0.4.0", optional = true }
|
|
||||||
indexmap = "1.9.1"
|
|
||||||
lasso = "0.6"
|
lasso = "0.6"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
# todo: no commandline by default
|
||||||
default = ["commandline", "random"]
|
default = ["commandline", "random"]
|
||||||
# Option (enabled by default): build a binary using clap
|
# Option (enabled by default): build a binary using clap
|
||||||
commandline = ["clap"]
|
commandline = ["clap"]
|
||||||
# Option: enable nightly-only features (for right now, only the `track_caller` attribute)
|
|
||||||
nightly = []
|
|
||||||
# Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()`
|
# Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()`
|
||||||
random = ["rand"]
|
random = ["rand"]
|
||||||
# Option: expose JavaScript-friendly WebAssembly exports
|
# Option: expose JavaScript-friendly WebAssembly exports
|
||||||
wasm-exports = ["wasm-bindgen"]
|
wasm-exports = ["wasm-bindgen"]
|
||||||
# Option: enable features that assist in profiling (e.g. inline(never))
|
|
||||||
profiling = []
|
|
||||||
# Option: enable criterion for benchmarking
|
|
||||||
bench = ["criterion"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
|
48
README.md
48
README.md
@ -3,17 +3,12 @@
|
|||||||
This crate aims to provide a high level interface for compiling Sass into
|
This crate aims to provide a high level interface for compiling Sass into
|
||||||
plain CSS. It offers a very limited API, currently exposing only 2 functions.
|
plain CSS. It offers a very limited API, currently exposing only 2 functions.
|
||||||
|
|
||||||
In addition to a library, also included is a binary that is intended to act as an invisible
|
In addition to a library, this crate also includes a binary that is intended to act as an invisible
|
||||||
replacement to the Sass commandline executable.
|
replacement to the Sass commandline executable.
|
||||||
|
|
||||||
This crate aims to achieve complete feature parity with the `dart-sass` reference
|
This crate aims to achieve complete feature parity with the `dart-sass` reference
|
||||||
implementation. A deviation from the `dart-sass` implementation can be considered
|
implementation. A deviation from the `dart-sass` implementation can be considered
|
||||||
a bug except for in the following situations:
|
a bug except for in the case of error message and error spans.
|
||||||
|
|
||||||
- Error messages
|
|
||||||
- Error spans
|
|
||||||
- Certain aspects of the indented syntax
|
|
||||||
- Potentially others in the future
|
|
||||||
|
|
||||||
[Documentation](https://docs.rs/grass/)
|
[Documentation](https://docs.rs/grass/)
|
||||||
[crates.io](https://crates.io/crates/grass)
|
[crates.io](https://crates.io/crates/grass)
|
||||||
@ -24,17 +19,7 @@ a bug except for in the following situations:
|
|||||||
|
|
||||||
Every commit of `grass` is tested against bootstrap v5.0.2, and every release is tested against the last 2,500 commits of bootstrap's `main` branch.
|
Every commit of `grass` is tested against bootstrap v5.0.2, and every release is tested against the last 2,500 commits of bootstrap's `main` branch.
|
||||||
|
|
||||||
That said, there are a number of known missing features and bugs. The notable features remaining are
|
That said, there are a number of known missing features and bugs. The rough edges of `grass` largely include `@forward` and more complex uses of `@uses`. We support basic usage of these rules, but more advanced features such as `@import`ing modules containing `@forward` with prefixes may not behave as expected.
|
||||||
|
|
||||||
```
|
|
||||||
indented syntax
|
|
||||||
@forward and more complex uses of @use
|
|
||||||
@at-root and @import media queries
|
|
||||||
@media query merging
|
|
||||||
/ as a separator in color functions, e.g. rgba(255, 255, 255 / 0)
|
|
||||||
Infinity and -Infinity
|
|
||||||
builtin meta function `keywords`
|
|
||||||
```
|
|
||||||
|
|
||||||
All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19).
|
All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19).
|
||||||
|
|
||||||
@ -44,11 +29,10 @@ All known missing features and bugs are tracked in [#19](https://github.com/conn
|
|||||||
|
|
||||||
`grass` experimentally releases a
|
`grass` experimentally releases a
|
||||||
[WASM version of the library to npm](https://www.npmjs.com/package/@connorskees/grass),
|
[WASM version of the library to npm](https://www.npmjs.com/package/@connorskees/grass),
|
||||||
compiled using wasm-bindgen. To use `grass` in your JavaScript projects, just run
|
compiled using wasm-bindgen. To use `grass` in your JavaScript projects, run
|
||||||
`npm install @connorskees/grass` to add it to your package.json. Better documentation
|
`npm install @connorskees/grass` to add it to your package.json. This version of grass is not currently well documented, but one can find example usage in the [`grassmeister` repository](https://github.com/connorskees/grassmeister).
|
||||||
for this version will be provided when the library becomes more stable.
|
|
||||||
|
|
||||||
## Features
|
## Cargo Features
|
||||||
|
|
||||||
### commandline
|
### commandline
|
||||||
|
|
||||||
@ -73,28 +57,16 @@ are in the official spec.
|
|||||||
|
|
||||||
Having said that, to run the official test suite,
|
Having said that, to run the official test suite,
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/connorskees/grass --recursive
|
|
||||||
cd grass
|
|
||||||
cargo b --release
|
|
||||||
./sass-spec/sass-spec.rb -c './target/release/grass'
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: you will have to install [ruby](https://www.ruby-lang.org/en/downloads/),
|
|
||||||
[bundler](https://bundler.io/) and run `bundle install` in `./sass-spec/`.
|
|
||||||
This might also require you to install the requirements separately
|
|
||||||
for [curses](https://github.com/ruby/curses).
|
|
||||||
|
|
||||||
Alternatively, it is possible to use nodejs to run the spec,
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# This script expects node >=v14.14.0. Check version with `node --version`
|
# This script expects node >=v14.14.0. Check version with `node --version`
|
||||||
git clone https://github.com/connorskees/grass --recursive
|
git clone https://github.com/connorskees/grass --recursive
|
||||||
cd grass && cargo b --release
|
cd grass && cargo b --release
|
||||||
cd sass-spec && npm install
|
cd sass-spec && npm install
|
||||||
npm run sass-spec -- --command '../target/release/grass'
|
npm run sass-spec -- --impl=dart-sass --command '../target/release/grass'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The spec runner does not work on Windows.
|
||||||
|
|
||||||
These numbers come from a default run of the Sass specification as shown above.
|
These numbers come from a default run of the Sass specification as shown above.
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -103,3 +75,5 @@ PASSING: 4205
|
|||||||
FAILING: 2051
|
FAILING: 2051
|
||||||
TOTAL: 6256
|
TOTAL: 6256
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- todo: msrv 1.41.1 -->
|
@ -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 {
|
a {
|
||||||
background: red;
|
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::{self, Write};
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::{parse::Stmt, selector::Selector};
|
use codemap::Span;
|
||||||
|
|
||||||
|
use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser, token::Token};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct MediaRule {
|
pub(crate) struct MediaRule {
|
||||||
pub super_selector: Selector,
|
pub query: Vec<MediaQuery>,
|
||||||
pub query: String,
|
pub body: Vec<CssStmt>,
|
||||||
pub body: Vec<Stmt>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub(crate) struct MediaQuery {
|
pub(crate) struct MediaQuery {
|
||||||
/// The modifier, probably either "not" or "only".
|
|
||||||
///
|
|
||||||
/// This may be `None` if no modifier is in use.
|
|
||||||
pub modifier: Option<String>,
|
pub modifier: Option<String>,
|
||||||
|
|
||||||
/// The media type, for example "screen" or "print".
|
|
||||||
///
|
|
||||||
/// This may be `None`. If so, `self.features` will not be empty.
|
|
||||||
pub media_type: Option<String>,
|
pub media_type: Option<String>,
|
||||||
|
pub conditions: Vec<String>,
|
||||||
/// Feature queries, including parentheses.
|
pub conjunction: bool,
|
||||||
pub features: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaQuery {
|
impl MediaQuery {
|
||||||
pub fn is_condition(&self) -> bool {
|
|
||||||
self.modifier.is_none() && self.media_type.is_none()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches_all_types(&self) -> bool {
|
pub fn matches_all_types(&self) -> bool {
|
||||||
self.media_type.is_none()
|
self.media_type.is_none()
|
||||||
|| self
|
|| self
|
||||||
@ -39,16 +27,44 @@ impl MediaQuery {
|
|||||||
.map_or(false, |v| v.to_ascii_lowercase() == "all")
|
.map_or(false, |v| v.to_ascii_lowercase() == "all")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn condition(features: Vec<String>) -> Self {
|
pub fn condition(
|
||||||
|
conditions: Vec<String>,
|
||||||
|
// default=true
|
||||||
|
conjunction: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
modifier: None,
|
modifier: None,
|
||||||
media_type: None,
|
media_type: None,
|
||||||
features,
|
conditions,
|
||||||
|
conjunction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn media_type(
|
||||||
|
media_type: Option<String>,
|
||||||
|
modifier: Option<String>,
|
||||||
|
conditions: Option<Vec<String>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
modifier,
|
||||||
|
conjunction: true,
|
||||||
|
media_type,
|
||||||
|
conditions: conditions.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_list(list: &str, span: Span) -> SassResult<Vec<Self>> {
|
||||||
|
let toks = Lexer::new(list.chars().map(|x| Token::new(span, x)).collect());
|
||||||
|
|
||||||
|
MediaQueryParser::new(toks).parse()
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::if_not_else)]
|
#[allow(clippy::if_not_else)]
|
||||||
fn merge(&self, other: &Self) -> MediaQueryMergeResult {
|
pub fn merge(&self, other: &Self) -> MediaQueryMergeResult {
|
||||||
|
if !self.conjunction || !other.conjunction {
|
||||||
|
return MediaQueryMergeResult::Unrepresentable;
|
||||||
|
}
|
||||||
|
|
||||||
let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
||||||
let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase());
|
let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase());
|
||||||
let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase());
|
||||||
@ -56,33 +72,34 @@ impl MediaQuery {
|
|||||||
|
|
||||||
if this_type.is_none() && other_type.is_none() {
|
if this_type.is_none() && other_type.is_none() {
|
||||||
return MediaQueryMergeResult::Success(Self::condition(
|
return MediaQueryMergeResult::Success(Self::condition(
|
||||||
self.features
|
self.conditions
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&other.features)
|
.chain(&other.conditions)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
|
true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let modifier;
|
let modifier;
|
||||||
let media_type;
|
let media_type;
|
||||||
let features;
|
let conditions;
|
||||||
|
|
||||||
if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) {
|
if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) {
|
||||||
if this_modifier == other_modifier {
|
if this_modifier == other_modifier {
|
||||||
let negative_features = if this_modifier.as_deref() == Some("not") {
|
let negative_conditions = if this_modifier.as_deref() == Some("not") {
|
||||||
&self.features
|
&self.conditions
|
||||||
} else {
|
} else {
|
||||||
&other.features
|
&other.conditions
|
||||||
};
|
};
|
||||||
|
|
||||||
let positive_features = if this_modifier.as_deref() == Some("not") {
|
let positive_conditions = if this_modifier.as_deref() == Some("not") {
|
||||||
&other.features
|
&other.conditions
|
||||||
} else {
|
} else {
|
||||||
&self.features
|
&self.conditions
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the negative features are a subset of the positive features, the
|
// If the negative conditions are a subset of the positive conditions, the
|
||||||
// query is empty. For example, `not screen and (color)` has no
|
// query is empty. For example, `not screen and (color)` has no
|
||||||
// intersection with `screen and (color) and (grid)`.
|
// intersection with `screen and (color) and (grid)`.
|
||||||
//
|
//
|
||||||
@ -90,9 +107,9 @@ impl MediaQuery {
|
|||||||
// (grid)`, because it means `not (screen and (color))` and so it allows
|
// (grid)`, because it means `not (screen and (color))` and so it allows
|
||||||
// a screen with no color but with a grid.
|
// a screen with no color but with a grid.
|
||||||
|
|
||||||
if negative_features
|
if negative_conditions
|
||||||
.iter()
|
.iter()
|
||||||
.all(|feat| positive_features.contains(feat))
|
.all(|feat| positive_conditions.contains(feat))
|
||||||
{
|
{
|
||||||
return MediaQueryMergeResult::Empty;
|
return MediaQueryMergeResult::Empty;
|
||||||
}
|
}
|
||||||
@ -105,11 +122,11 @@ impl MediaQuery {
|
|||||||
if this_modifier.as_deref() == Some("not") {
|
if this_modifier.as_deref() == Some("not") {
|
||||||
modifier = &other_modifier;
|
modifier = &other_modifier;
|
||||||
media_type = &other_type;
|
media_type = &other_type;
|
||||||
features = other.features.clone();
|
conditions = other.conditions.clone();
|
||||||
} else {
|
} else {
|
||||||
modifier = &this_modifier;
|
modifier = &this_modifier;
|
||||||
media_type = &this_type;
|
media_type = &this_type;
|
||||||
features = self.features.clone();
|
conditions = self.conditions.clone();
|
||||||
}
|
}
|
||||||
} else if this_modifier.as_deref() == Some("not") {
|
} else if this_modifier.as_deref() == Some("not") {
|
||||||
debug_assert_eq!(other_modifier.as_deref(), Some("not"));
|
debug_assert_eq!(other_modifier.as_deref(), Some("not"));
|
||||||
@ -119,27 +136,27 @@ impl MediaQuery {
|
|||||||
return MediaQueryMergeResult::Unrepresentable;
|
return MediaQueryMergeResult::Unrepresentable;
|
||||||
}
|
}
|
||||||
|
|
||||||
let more_features = if self.features.len() > other.features.len() {
|
let more_conditions = if self.conditions.len() > other.conditions.len() {
|
||||||
&self.features
|
&self.conditions
|
||||||
} else {
|
} else {
|
||||||
&other.features
|
&other.conditions
|
||||||
};
|
};
|
||||||
|
|
||||||
let fewer_features = if self.features.len() > other.features.len() {
|
let fewer_conditions = if self.conditions.len() > other.conditions.len() {
|
||||||
&other.features
|
&other.conditions
|
||||||
} else {
|
} else {
|
||||||
&self.features
|
&self.conditions
|
||||||
};
|
};
|
||||||
|
|
||||||
// If one set of features is a superset of the other, use those features
|
// If one set of conditions is a superset of the other, use those conditions
|
||||||
// because they're strictly narrower.
|
// because they're strictly narrower.
|
||||||
if fewer_features
|
if fewer_conditions
|
||||||
.iter()
|
.iter()
|
||||||
.all(|feat| more_features.contains(feat))
|
.all(|feat| more_conditions.contains(feat))
|
||||||
{
|
{
|
||||||
modifier = &this_modifier; // "not"
|
modifier = &this_modifier;
|
||||||
media_type = &this_type;
|
media_type = &this_type;
|
||||||
features = more_features.clone();
|
conditions = more_conditions.clone();
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, there's no way to represent the intersection.
|
// Otherwise, there's no way to represent the intersection.
|
||||||
return MediaQueryMergeResult::Unrepresentable;
|
return MediaQueryMergeResult::Unrepresentable;
|
||||||
@ -155,19 +172,19 @@ impl MediaQuery {
|
|||||||
&other_type
|
&other_type
|
||||||
};
|
};
|
||||||
|
|
||||||
features = self
|
conditions = self
|
||||||
.features
|
.conditions
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&other.features)
|
.chain(&other.conditions)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
} else if other.matches_all_types() {
|
} else if other.matches_all_types() {
|
||||||
modifier = &this_modifier;
|
modifier = &this_modifier;
|
||||||
media_type = &this_type;
|
media_type = &this_type;
|
||||||
features = self
|
conditions = self
|
||||||
.features
|
.conditions
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&other.features)
|
.chain(&other.conditions)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
} else if this_type != other_type {
|
} else if this_type != other_type {
|
||||||
@ -180,10 +197,10 @@ impl MediaQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
media_type = &this_type;
|
media_type = &this_type;
|
||||||
features = self
|
conditions = self
|
||||||
.features
|
.conditions
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&other.features)
|
.chain(&other.conditions)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
@ -199,7 +216,8 @@ impl MediaQuery {
|
|||||||
} else {
|
} else {
|
||||||
other.modifier.clone()
|
other.modifier.clone()
|
||||||
},
|
},
|
||||||
features,
|
conditions,
|
||||||
|
conjunction: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,19 +226,22 @@ impl fmt::Display for MediaQuery {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if let Some(modifier) = &self.modifier {
|
if let Some(modifier) = &self.modifier {
|
||||||
f.write_str(modifier)?;
|
f.write_str(modifier)?;
|
||||||
|
f.write_char(' ')?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(media_type) = &self.media_type {
|
if let Some(media_type) = &self.media_type {
|
||||||
f.write_str(media_type)?;
|
f.write_str(media_type)?;
|
||||||
if !&self.features.is_empty() {
|
if !&self.conditions.is_empty() {
|
||||||
f.write_str(" and ")?;
|
f.write_str(" and ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.write_str(&self.features.join(" and "))
|
|
||||||
|
f.write_str(&self.conditions.join(" and "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
enum MediaQueryMergeResult {
|
pub(crate) enum MediaQueryMergeResult {
|
||||||
Empty,
|
Empty,
|
||||||
Unrepresentable,
|
Unrepresentable,
|
||||||
Success(MediaQuery),
|
Success(MediaQuery),
|
32
src/ast/mixin.rs
Normal file
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)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) struct UnknownAtRule {
|
pub(crate) struct UnknownAtRule {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub super_selector: Selector,
|
// pub super_selector: Selector,
|
||||||
pub params: String,
|
pub params: String,
|
||||||
pub body: Vec<Stmt>,
|
pub body: Vec<CssStmt>,
|
||||||
|
|
||||||
/// Whether or not this @-rule was declared with curly
|
/// Whether or not this @-rule was declared with curly
|
||||||
/// braces. A body may not necessarily have contents
|
/// braces. A body may not necessarily have contents
|
@ -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 crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
|
||||||
use num_traits::One;
|
|
||||||
|
|
||||||
use crate::{
|
use super::rgb::{function_string, parse_channels, percentage_or_unitless, ParsedChannels};
|
||||||
args::CallArgs,
|
|
||||||
color::Color,
|
|
||||||
common::{Brackets, ListSeparator, QuoteKind},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn hsl_3_args(
|
||||||
if args.is_empty() {
|
name: &'static str,
|
||||||
return Err(("Missing argument $channels.", args.span()).into());
|
mut args: ArgumentResult,
|
||||||
}
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
|
let span = args.span();
|
||||||
|
|
||||||
let len = args.len();
|
|
||||||
|
|
||||||
if len == 1 {
|
|
||||||
let mut channels = match args.get_err(0, "channels")? {
|
|
||||||
Value::List(v, ..) => v,
|
|
||||||
v if v.is_special_function() => vec![v],
|
|
||||||
_ => return Err(("Missing argument $channels.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if channels.len() > 3 {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"Only 3 elements allowed, but {} were passed.",
|
|
||||||
channels.len()
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if channels.iter().any(Value::is_special_function) {
|
|
||||||
let channel_sep = if channels.len() < 3 {
|
|
||||||
ListSeparator::Space
|
|
||||||
} else {
|
|
||||||
ListSeparator::Comma
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(Value::String(
|
|
||||||
format!(
|
|
||||||
"{}({})",
|
|
||||||
name,
|
|
||||||
Value::List(channels, channel_sep, Brackets::None)
|
|
||||||
.to_css_string(args.span(), false)?
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let lightness = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$lightness: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $lightness.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let saturation = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), ..)) => n / Number::from(100),
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$saturation: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $saturation.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let hue = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), ..)) => n,
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $hue.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Value::Color(Box::new(Color::from_hsla(
|
|
||||||
hue,
|
|
||||||
saturation,
|
|
||||||
lightness,
|
|
||||||
Number::one(),
|
|
||||||
))))
|
|
||||||
} else {
|
|
||||||
let hue = args.get_err(0, "hue")?;
|
let hue = args.get_err(0, "hue")?;
|
||||||
let saturation = args.get_err(1, "saturation")?;
|
let saturation = args.get_err(1, "saturation")?;
|
||||||
let lightness = args.get_err(2, "lightness")?;
|
let lightness = args.get_err(2, "lightness")?;
|
||||||
let alpha = args.default_arg(
|
let alpha = args.default_arg(
|
||||||
3,
|
3,
|
||||||
"alpha",
|
"alpha",
|
||||||
Value::Dimension(Some(Number::one()), Unit::None, true),
|
Value::Dimension(SassNumber {
|
||||||
)?;
|
num: (Number::one()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if [&hue, &saturation, &lightness, &alpha]
|
if [&hue, &saturation, &lightness, &alpha]
|
||||||
.iter()
|
.iter()
|
||||||
@ -121,7 +34,7 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
|
|||||||
"{}({})",
|
"{}({})",
|
||||||
name,
|
name,
|
||||||
Value::List(
|
Value::List(
|
||||||
if len == 4 {
|
if args.len() == 4 {
|
||||||
vec![hue, saturation, lightness, alpha]
|
vec![hue, saturation, lightness, alpha]
|
||||||
} else {
|
} else {
|
||||||
vec![hue, saturation, lightness]
|
vec![hue, saturation, lightness]
|
||||||
@ -135,85 +48,89 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> Sas
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hue = match hue {
|
let hue = hue.assert_number_with_name("hue", span)?;
|
||||||
Value::Dimension(Some(n), ..) => n,
|
let saturation = saturation.assert_number_with_name("saturation", span)?;
|
||||||
Value::Dimension(None, ..) => todo!(),
|
let lightness = lightness.assert_number_with_name("lightness", span)?;
|
||||||
v => {
|
let alpha = percentage_or_unitless(
|
||||||
return Err((
|
&alpha.assert_number_with_name("alpha", span)?,
|
||||||
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
1.0,
|
||||||
args.span(),
|
"alpha",
|
||||||
)
|
span,
|
||||||
.into())
|
visitor,
|
||||||
}
|
)?;
|
||||||
};
|
|
||||||
let saturation = match saturation {
|
Ok(Value::Color(Box::new(Color::from_hsla_fn(
|
||||||
Value::Dimension(Some(n), ..) => n / Number::from(100),
|
Number(hue.num().rem_euclid(360.0)),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
saturation.num() / Number::from(100),
|
||||||
v => {
|
lightness.num() / Number::from(100),
|
||||||
return Err((
|
Number(alpha),
|
||||||
format!(
|
|
||||||
"$saturation: {} is not a number.",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let lightness = match lightness {
|
|
||||||
Value::Dimension(Some(n), ..) => n / Number::from(100),
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$lightness: {} is not a number.",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let alpha = match alpha {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$alpha: Expected {} to have no units or \"%\".",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(Color::from_hsla(
|
|
||||||
hue, saturation, lightness, alpha,
|
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inner_hsl(
|
||||||
|
name: &'static str,
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
|
args.max_args(4)?;
|
||||||
|
let span = args.span();
|
||||||
|
|
||||||
|
let len = args.len();
|
||||||
|
|
||||||
|
if len == 1 || len == 0 {
|
||||||
|
match parse_channels(
|
||||||
|
name,
|
||||||
|
&["hue", "saturation", "lightness"],
|
||||||
|
args.get_err(0, "channels")?,
|
||||||
|
visitor,
|
||||||
|
args.span(),
|
||||||
|
)? {
|
||||||
|
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
|
||||||
|
ParsedChannels::List(list) => {
|
||||||
|
let args = ArgumentResult {
|
||||||
|
positional: list,
|
||||||
|
named: BTreeMap::new(),
|
||||||
|
separator: ListSeparator::Comma,
|
||||||
|
span: args.span(),
|
||||||
|
touched: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
hsl_3_args(name, args, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len == 2 {
|
||||||
|
let hue = args.get_err(0, "hue")?;
|
||||||
|
let saturation = args.get_err(1, "saturation")?;
|
||||||
|
|
||||||
|
if hue.is_var() || saturation.is_var() {
|
||||||
|
return Ok(Value::String(
|
||||||
|
function_string(name, &[hue, saturation], visitor, span)?,
|
||||||
|
QuoteKind::None,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Err(("Missing argument $lightness.", args.span()).into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return hsl_3_args(name, args, visitor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hsl(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
inner_hsl("hsl", args, parser)
|
inner_hsl("hsl", args, visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hsla(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
inner_hsl("hsla", args, parser)
|
inner_hsl("hsla", args, visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.hue()), Unit::Deg, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.hue()),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -222,10 +139,14 @@ pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.saturation()), Unit::Percent, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.saturation()),
|
||||||
|
unit: Unit::Percent,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -234,10 +155,14 @@ pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.lightness()), Unit::Percent, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: c.lightness(),
|
||||||
|
unit: Unit::Percent,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -246,36 +171,20 @@ pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = args
|
||||||
Value::Color(c) => c,
|
.get_err(0, "color")?
|
||||||
v => {
|
.assert_color_with_name("color", args.span())?;
|
||||||
return Err((
|
let degrees = args
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
.get_err(1, "degrees")?
|
||||||
args.span(),
|
.assert_number_with_name("degrees", args.span())?
|
||||||
)
|
.num();
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let degrees = match args.get_err(1, "degrees")? {
|
|
||||||
Value::Dimension(Some(n), ..) => n,
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$degrees: {} is not a number.",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(color.adjust_hue(degrees))))
|
Ok(Value::Color(Box::new(color.adjust_hue(degrees))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -287,24 +196,16 @@ fn lighten(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let amount = match args.get_err(1, "amount")? {
|
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
let amount = args
|
||||||
Value::Dimension(None, ..) => todo!(),
|
.get_err(1, "amount")?
|
||||||
v => {
|
.assert_number_with_name("amount", args.span())?;
|
||||||
return Err((
|
let amount = bound!(args, "amount", amount.num(), amount.unit, 0, 100) / Number(100.0);
|
||||||
format!(
|
|
||||||
"$amount: {} is not a number.",
|
|
||||||
v.to_css_string(args.span(), false)?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(color.lighten(amount))))
|
Ok(Value::Color(Box::new(color.lighten(amount))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -317,8 +218,12 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let amount = match args.get_err(1, "amount")? {
|
let amount = match args.get_err(1, "amount")? {
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
@ -333,22 +238,29 @@ fn darken(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
Ok(Value::Color(Box::new(color.darken(amount))))
|
Ok(Value::Color(Box::new(color.darken(amount))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
|
let amount = args
|
||||||
|
.get_err(0, "amount")?
|
||||||
|
.assert_number_with_name("amount", args.span())?;
|
||||||
|
|
||||||
return Ok(Value::String(
|
return Ok(Value::String(
|
||||||
format!(
|
format!(
|
||||||
"saturate({})",
|
"saturate({})",
|
||||||
args.get_err(0, "amount")?
|
serialize_number(&amount, &Options::default(), args.span())?,
|
||||||
.to_css_string(args.span(), false)?
|
|
||||||
),
|
),
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount = match args.get_err(1, "amount")? {
|
let amount = match args.get_err(1, "amount")? {
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
@ -362,11 +274,16 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
Value::Dimension(Some(n), u, _) => {
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => {
|
||||||
|
// todo: this branch should be superfluous/incorrect
|
||||||
return Ok(Value::String(
|
return Ok(Value::String(
|
||||||
format!("saturate({}{})", n.inspect(), u),
|
format!("saturate({}{})", n.inspect(), u),
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
@ -379,7 +296,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
Ok(Value::Color(Box::new(color.saturate(amount))))
|
Ok(Value::Color(Box::new(color.saturate(amount))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -392,13 +309,17 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let amount = match args.get_err(1, "amount")? {
|
let amount = match args.get_err(1, "amount")? {
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => bound!(args, "amount", n, u, 0, 100) / Number::from(100),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$amount: {} is not a number.",
|
"$amount: {} is not a number.",
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||||
),
|
),
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
@ -408,11 +329,15 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
Ok(Value::Color(Box::new(color.desaturate(amount))))
|
Ok(Value::Color(Box::new(color.desaturate(amount))))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
Value::Dimension(Some(n), u, _) => {
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => {
|
||||||
return Ok(Value::String(
|
return Ok(Value::String(
|
||||||
format!("grayscale({}{})", n.inspect(), u),
|
format!("grayscale({}{})", n.inspect(), u),
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
@ -429,7 +354,7 @@ pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
Ok(Value::Color(Box::new(color.desaturate(Number::one()))))
|
Ok(Value::Color(Box::new(color.desaturate(Number::one()))))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -444,24 +369,28 @@ pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
Ok(Value::Color(Box::new(color.complement())))
|
Ok(Value::Color(Box::new(color.complement())))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let weight = match args.get(1, "weight") {
|
let weight = match args.get(1, "weight") {
|
||||||
Some(Err(e)) => return Err(e),
|
Some(Spanned {
|
||||||
Some(Ok(Spanned {
|
node: Value::Dimension(SassNumber { num: n, .. }),
|
||||||
node: Value::Dimension(Some(n), u, _),
|
|
||||||
..
|
..
|
||||||
})) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
|
}) if n.is_nan() => todo!(),
|
||||||
Some(Ok(Spanned {
|
Some(Spanned {
|
||||||
node: Value::Dimension(None, ..),
|
node:
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}),
|
||||||
..
|
..
|
||||||
})) => todo!(),
|
}) => Some(bound!(args, "weight", n, u, 0, 100) / Number::from(100)),
|
||||||
None => None,
|
None => None,
|
||||||
Some(Ok(v)) => {
|
Some(v) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$weight: {} is not a number.",
|
"$weight: {} is not a number.",
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||||
),
|
),
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
@ -472,7 +401,11 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
|||||||
Value::Color(c) => Ok(Value::Color(Box::new(
|
Value::Color(c) => Ok(Value::Color(Box::new(
|
||||||
c.invert(weight.unwrap_or_else(Number::one)),
|
c.invert(weight.unwrap_or_else(Number::one)),
|
||||||
))),
|
))),
|
||||||
Value::Dimension(Some(n), u, _) => {
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => {
|
||||||
if weight.is_some() {
|
if weight.is_some() {
|
||||||
return Err((
|
return Err((
|
||||||
"Only one argument may be passed to the plain-CSS invert() function.",
|
"Only one argument may be passed to the plain-CSS invert() function.",
|
||||||
@ -485,9 +418,6 @@ pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
|||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Value::Dimension(None, u, _) => {
|
|
||||||
Ok(Value::String(format!("invert(NaN{})", u), QuoteKind::None))
|
|
||||||
}
|
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
use num_traits::One;
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use crate::{
|
use super::rgb::{parse_channels, ParsedChannels};
|
||||||
args::CallArgs,
|
|
||||||
color::Color,
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
|
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
@ -26,39 +19,36 @@ pub(crate) fn blackness(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
let blackness =
|
let blackness =
|
||||||
Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255));
|
Number::from(1) - (color.red().max(color.green()).max(color.blue()) / Number::from(255));
|
||||||
|
|
||||||
Ok(Value::Dimension(Some(blackness * 100), Unit::Percent, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (blackness * 100),
|
||||||
|
unit: Unit::Percent,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn whiteness(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
|
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = args
|
||||||
Value::Color(c) => c,
|
.get_err(0, "color")?
|
||||||
v => {
|
.assert_color_with_name("color", args.span())?;
|
||||||
return Err((
|
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255);
|
let whiteness = color.red().min(color.green()).min(color.blue()) / Number::from(255);
|
||||||
|
|
||||||
Ok(Value::Dimension(Some(whiteness * 100), Unit::Percent, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (whiteness * 100),
|
||||||
|
unit: Unit::Percent,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(4)?;
|
let span = args.span();
|
||||||
|
|
||||||
if args.is_empty() {
|
|
||||||
return Err(("Missing argument $channels.", args.span()).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let hue = match args.get(0, "hue") {
|
let hue = match args.get(0, "hue") {
|
||||||
Some(Ok(v)) => match v.node {
|
Some(v) => match v.node {
|
||||||
Value::Dimension(Some(n), ..) => n,
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber { num: n, .. }) => n,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
format!("$hue: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -67,57 +57,28 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Err(e)) => return Err(e),
|
|
||||||
None => return Err(("Missing element $hue.", args.span()).into()),
|
None => return Err(("Missing element $hue.", args.span()).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let whiteness = match args.get(1, "whiteness") {
|
let whiteness = args
|
||||||
Some(Ok(v)) => match v.node {
|
.get_err(1, "whiteness")?
|
||||||
Value::Dimension(Some(n), Unit::Percent, ..) => n,
|
.assert_number_with_name("whiteness", span)?;
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$whiteness: Expected {} to have unit \"%\".",
|
|
||||||
v.inspect(args.span())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$whiteness: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(e)) => return Err(e),
|
|
||||||
None => return Err(("Missing element $whiteness.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let blackness = match args.get(2, "blackness") {
|
let blackness = args
|
||||||
Some(Ok(v)) => match v.node {
|
.get_err(2, "blackness")?
|
||||||
Value::Dimension(Some(n), ..) => n,
|
.assert_number_with_name("blackness", span)?;
|
||||||
Value::Dimension(None, ..) => todo!(),
|
blackness.assert_unit(&Unit::Percent, "blackness", span)?;
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$blackness: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(e)) => return Err(e),
|
|
||||||
None => return Err(("Missing element $blackness.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let alpha = match args.get(3, "alpha") {
|
let alpha = match args.get(3, "alpha") {
|
||||||
Some(Ok(v)) => match v.node {
|
Some(v) => match v.node {
|
||||||
Value::Dimension(Some(n), Unit::Percent, ..) => n / Number::from(100),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(Some(n), ..) => n,
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(None, ..) => todo!(),
|
num: n,
|
||||||
|
unit: Unit::Percent,
|
||||||
|
..
|
||||||
|
}) => n / Number::from(100),
|
||||||
|
Value::Dimension(SassNumber { num: n, .. }) => n,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -126,11 +87,44 @@ pub(crate) fn hwb(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Err(e)) => return Err(e),
|
|
||||||
None => Number::one(),
|
None => Number::one(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::Color(Box::new(Color::from_hwb(
|
Ok(Value::Color(Box::new(Color::from_hwb(
|
||||||
hue, whiteness, blackness, alpha,
|
hue,
|
||||||
|
whiteness.num,
|
||||||
|
blackness.num,
|
||||||
|
alpha,
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
args.max_args(4)?;
|
||||||
|
|
||||||
|
if args.len() == 0 || args.len() == 1 {
|
||||||
|
match parse_channels(
|
||||||
|
"hwb",
|
||||||
|
&["hue", "whiteness", "blackness"],
|
||||||
|
args.get_err(0, "channels")?,
|
||||||
|
visitor,
|
||||||
|
args.span(),
|
||||||
|
)? {
|
||||||
|
ParsedChannels::String(s) => {
|
||||||
|
Err((format!("Expected numeric channels, got {}", s), args.span()).into())
|
||||||
|
}
|
||||||
|
ParsedChannels::List(list) => {
|
||||||
|
let args = ArgumentResult {
|
||||||
|
positional: list,
|
||||||
|
named: BTreeMap::new(),
|
||||||
|
separator: ListSeparator::Comma,
|
||||||
|
span: args.span(),
|
||||||
|
touched: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
hwb_inner(args, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hwb_inner(args, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use super::GlobalFunctionMap;
|
||||||
|
|
||||||
pub mod hsl;
|
pub mod hsl;
|
||||||
pub mod hwb;
|
pub mod hwb;
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs, common::QuoteKind, error::SassResult, parse::Parser, unit::Unit, value::Number,
|
|
||||||
value::Value,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
|
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
|
||||||
fn is_ms_filter(s: &str) -> bool {
|
fn is_ms_filter(s: &str) -> bool {
|
||||||
@ -35,10 +30,14 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
if args.len() <= 1 {
|
if args.len() <= 1 {
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.alpha()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
|
Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
|
||||||
Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
|
Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
|
||||||
}
|
}
|
||||||
@ -69,15 +68,23 @@ pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(Some(num), unit, _) => Ok(Value::String(
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.alpha()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num,
|
||||||
|
unit,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok(Value::String(
|
||||||
format!("opacity({}{})", num.inspect(), unit),
|
format!("opacity({}{})", num.inspect(), unit),
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
)),
|
)),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -86,8 +93,7 @@ pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: unify `opacify` and `fade_in`
|
fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -99,21 +105,16 @@ fn opacify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let amount = match args.get_err(1, "amount")? {
|
let amount = args
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
.get_err(1, "amount")?
|
||||||
Value::Dimension(None, ..) => todo!(),
|
.assert_number_with_name("amount", args.span())?;
|
||||||
v => {
|
|
||||||
return Err((
|
let amount = bound!(args, "amount", amount.num(), amount.unit(), 0, 1);
|
||||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(color.fade_in(amount))))
|
Ok(Value::Color(Box::new(color.fade_in(amount))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -126,61 +127,12 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let amount = match args.get_err(1, "amount")? {
|
let amount = match args.get_err(1, "amount")? {
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber {
|
||||||
v => {
|
num: n,
|
||||||
return Err((
|
unit: u,
|
||||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
as_slash: _,
|
||||||
args.span(),
|
}) => bound!(args, "amount", n, u, 0, 1),
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(color.fade_in(amount))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: unify with `fade_out`
|
|
||||||
fn transparentize(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(2)?;
|
|
||||||
let color = match args.get_err(0, "color")? {
|
|
||||||
Value::Color(c) => c,
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let amount = match args.get_err(1, "amount")? {
|
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(color.fade_out(amount))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fade_out(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(2)?;
|
|
||||||
let color = match args.get_err(0, "color")? {
|
|
||||||
Value::Color(c) => c,
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let amount = match args.get_err(1, "amount")? {
|
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1),
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
format!("$amount: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -196,7 +148,7 @@ pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
|||||||
f.insert("alpha", Builtin::new(alpha));
|
f.insert("alpha", Builtin::new(alpha));
|
||||||
f.insert("opacity", Builtin::new(opacity));
|
f.insert("opacity", Builtin::new(opacity));
|
||||||
f.insert("opacify", Builtin::new(opacify));
|
f.insert("opacify", Builtin::new(opacify));
|
||||||
f.insert("fade-in", Builtin::new(fade_in));
|
f.insert("fade-in", Builtin::new(opacify));
|
||||||
f.insert("transparentize", Builtin::new(transparentize));
|
f.insert("transparentize", Builtin::new(transparentize));
|
||||||
f.insert("fade-out", Builtin::new(fade_out));
|
f.insert("fade-out", Builtin::new(transparentize));
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,12 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use num_traits::{One, Signed, Zero};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs,
|
|
||||||
color::Color,
|
|
||||||
common::QuoteKind,
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
macro_rules! opt_rgba {
|
macro_rules! opt_rgba {
|
||||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||||
Value::Dimension(Some(n), u, _) => Some(bound!($args, $arg, n, u, $low, $high)),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n, unit: u, ..
|
||||||
|
}) => Some(bound!($args, $arg, n, u, $low, $high)),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
@ -31,11 +21,11 @@ macro_rules! opt_rgba {
|
|||||||
|
|
||||||
macro_rules! opt_hsl {
|
macro_rules! opt_hsl {
|
||||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||||
Value::Dimension(Some(n), u, _) => {
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100))
|
Value::Dimension(SassNumber {
|
||||||
}
|
num: n, unit: u, ..
|
||||||
Value::Dimension(None, ..) => todo!(),
|
}) => Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
@ -48,8 +38,8 @@ macro_rules! opt_hsl {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
if args.positional_arg(1).is_some() {
|
if args.get_positional(1).is_some() {
|
||||||
return Err((
|
return Err((
|
||||||
"Only one positional argument is allowed. All other arguments must be passed by name.",
|
"Only one positional argument is allowed. All other arguments must be passed by name.",
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -82,9 +72,9 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hue = match args.default_named_arg("hue", Value::Null)? {
|
let hue = match args.default_named_arg("hue", Value::Null) {
|
||||||
Value::Dimension(Some(n), ..) => Some(n),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
@ -116,7 +106,7 @@ pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
v => {
|
v => {
|
||||||
@ -142,9 +132,9 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hue = match args.default_named_arg("hue", Value::Null)? {
|
let hue = match args.default_named_arg("hue", Value::Null) {
|
||||||
Value::Dimension(Some(n), ..) => Some(n),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Value::Dimension(None, ..) => todo!(),
|
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
@ -179,12 +169,12 @@ pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
// todo: refactor into rgb and hsl?
|
// todo: refactor into rgb and hsl?
|
||||||
pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
|
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
|
||||||
if by.is_zero() {
|
if by.is_zero() {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
val.clone() + (if by.is_positive() { max - val } else { val }) * by
|
val + (if by.is_positive() { max - val } else { val }) * by
|
||||||
}
|
}
|
||||||
|
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
@ -201,12 +191,14 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
|
|
||||||
macro_rules! opt_scale_arg {
|
macro_rules! opt_scale_arg {
|
||||||
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
|
||||||
let $name = match $args.default_named_arg($arg, Value::Null)? {
|
let $name = match $args.default_named_arg($arg, Value::Null) {
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100))
|
Value::Dimension(SassNumber {
|
||||||
}
|
num: n,
|
||||||
Value::Dimension(None, ..) => todo!(),
|
unit: Unit::Percent,
|
||||||
v @ Value::Dimension(..) => {
|
..
|
||||||
|
}) => Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)),
|
||||||
|
v @ Value::Dimension { .. } => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"${}: Expected {} to have unit \"%\".",
|
"${}: Expected {} to have unit \"%\".",
|
||||||
@ -293,7 +285,7 @@ pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ie_hex_str(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let color = match args.get_err(0, "color")? {
|
let color = match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
|
@ -1,368 +1,365 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::{builtin::builtin_imports::*, serializer::inspect_number, value::fuzzy_round};
|
||||||
|
|
||||||
use num_traits::One;
|
pub(crate) fn function_string(
|
||||||
|
name: &'static str,
|
||||||
|
args: &[Value],
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
span: Span,
|
||||||
|
) -> SassResult<String> {
|
||||||
|
let args = args
|
||||||
|
.iter()
|
||||||
|
.map(|arg| arg.to_css_string(span, visitor.options.is_compressed()))
|
||||||
|
.collect::<SassResult<Vec<_>>>()?
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
use crate::{
|
Ok(format!("{}({})", name, args))
|
||||||
args::CallArgs,
|
|
||||||
color::Color,
|
|
||||||
common::{Brackets, ListSeparator, QuoteKind},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// name: Either `rgb` or `rgba` depending on the caller
|
|
||||||
// todo: refactor into smaller functions
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
|
||||||
fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
if args.is_empty() {
|
|
||||||
return Err(("Missing argument $channels.", args.span()).into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = args.len();
|
fn inner_rgb_2_arg(
|
||||||
|
name: &'static str,
|
||||||
if len == 1 {
|
mut args: ArgumentResult,
|
||||||
let mut channels = match args.get_err(0, "channels")? {
|
visitor: &mut Visitor,
|
||||||
Value::List(v, ..) => v,
|
) -> SassResult<Value> {
|
||||||
v if v.is_special_function() => vec![v],
|
// rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
|
||||||
_ => return Err(("Missing argument $channels.", args.span()).into()),
|
// and functions are parsed after variable substitution.
|
||||||
};
|
|
||||||
|
|
||||||
if channels.len() > 3 {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"Only 3 elements allowed, but {} were passed.",
|
|
||||||
channels.len()
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if channels.iter().any(Value::is_special_function) {
|
|
||||||
let channel_sep = if channels.len() < 3 {
|
|
||||||
ListSeparator::Space
|
|
||||||
} else {
|
|
||||||
ListSeparator::Comma
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(Value::String(
|
|
||||||
format!(
|
|
||||||
"{}({})",
|
|
||||||
name,
|
|
||||||
Value::List(channels, channel_sep, Brackets::None)
|
|
||||||
.to_css_string(args.span(), false)?
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let blue = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
|
||||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
|
||||||
(n / Number::from(100)) * Number::from(255)
|
|
||||||
}
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) if v.is_special_function() => {
|
|
||||||
let green = channels.pop().unwrap();
|
|
||||||
let red = channels.pop().unwrap();
|
|
||||||
return Ok(Value::String(
|
|
||||||
format!(
|
|
||||||
"{}({}, {}, {})",
|
|
||||||
name,
|
|
||||||
red.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
green.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$blue: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $blue.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let green = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
|
||||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
|
||||||
(n / Number::from(100)) * Number::from(255)
|
|
||||||
}
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) if v.is_special_function() => {
|
|
||||||
let string = match channels.pop() {
|
|
||||||
Some(red) => format!(
|
|
||||||
"{}({}, {}, {})",
|
|
||||||
name,
|
|
||||||
red.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
blue.to_string(parser.options.is_compressed())
|
|
||||||
),
|
|
||||||
None => format!(
|
|
||||||
"{}({} {})",
|
|
||||||
name,
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
blue.to_string(parser.options.is_compressed())
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return Ok(Value::String(string, QuoteKind::None));
|
|
||||||
}
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$green: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $green.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let red = match channels.pop() {
|
|
||||||
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
|
|
||||||
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
|
|
||||||
(n / Number::from(100)) * Number::from(255)
|
|
||||||
}
|
|
||||||
Some(Value::Dimension(None, ..)) => todo!(),
|
|
||||||
Some(v) if v.is_special_function() => {
|
|
||||||
return Ok(Value::String(
|
|
||||||
format!(
|
|
||||||
"{}({}, {}, {})",
|
|
||||||
name,
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?,
|
|
||||||
green.to_string(parser.options.is_compressed()),
|
|
||||||
blue.to_string(parser.options.is_compressed())
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Some(v) => {
|
|
||||||
return Err((
|
|
||||||
format!("$red: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
None => return Err(("Missing element $red.", args.span()).into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let color = Color::from_rgba(red, green, blue, Number::one());
|
|
||||||
|
|
||||||
Ok(Value::Color(Box::new(color)))
|
|
||||||
} else if len == 2 {
|
|
||||||
let color = args.get_err(0, "color")?;
|
let color = args.get_err(0, "color")?;
|
||||||
let alpha = args.get_err(1, "alpha")?;
|
let alpha = args.get_err(1, "alpha")?;
|
||||||
|
|
||||||
if color.is_special_function() || (alpha.is_special_function() && !color.is_color()) {
|
let is_compressed = visitor.options.is_compressed();
|
||||||
|
|
||||||
|
if color.is_var() {
|
||||||
return Ok(Value::String(
|
return Ok(Value::String(
|
||||||
format!(
|
function_string(name, &[color, alpha], visitor, args.span())?,
|
||||||
"{}({})",
|
|
||||||
name,
|
|
||||||
Value::List(vec![color, alpha], ListSeparator::Comma, Brackets::None)
|
|
||||||
.to_css_string(args.span(), false)?
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
));
|
));
|
||||||
}
|
} else if alpha.is_var() {
|
||||||
|
match &color {
|
||||||
let color = match color {
|
Value::Color(color) => {
|
||||||
Value::Color(c) => c,
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if alpha.is_special_function() {
|
|
||||||
return Ok(Value::String(
|
return Ok(Value::String(
|
||||||
format!(
|
format!(
|
||||||
"{}({}, {}, {}, {})",
|
"{}({}, {}, {}, {})",
|
||||||
name,
|
name,
|
||||||
color.red().to_string(false),
|
color.red().to_string(is_compressed),
|
||||||
color.green().to_string(false),
|
color.green().to_string(is_compressed),
|
||||||
color.blue().to_string(false),
|
color.blue().to_string(is_compressed),
|
||||||
alpha.to_css_string(args.span(), false)?,
|
alpha.to_css_string(args.span(), is_compressed)?
|
||||||
|
),
|
||||||
|
QuoteKind::None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Ok(Value::String(
|
||||||
|
function_string(name, &[color, alpha], visitor, args.span())?,
|
||||||
|
QuoteKind::None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if alpha.is_special_function() {
|
||||||
|
let color = color.assert_color_with_name("color", args.span())?;
|
||||||
|
|
||||||
|
return Ok(Value::String(
|
||||||
|
format!(
|
||||||
|
"{}({}, {}, {}, {})",
|
||||||
|
name,
|
||||||
|
color.red().to_string(is_compressed),
|
||||||
|
color.green().to_string(is_compressed),
|
||||||
|
color.blue().to_string(is_compressed),
|
||||||
|
alpha.to_css_string(args.span(), is_compressed)?
|
||||||
),
|
),
|
||||||
QuoteKind::None,
|
QuoteKind::None,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let alpha = match alpha {
|
let color = color.assert_color_with_name("color", args.span())?;
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
let alpha = alpha.assert_number_with_name("alpha", args.span())?;
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
|
Ok(Value::Color(Box::new(color.with_alpha(Number(
|
||||||
Value::Dimension(None, ..) => todo!(),
|
percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?,
|
||||||
v @ Value::Dimension(..) => {
|
)))))
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$alpha: Expected {} to have no units or \"%\".",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
}
|
||||||
v => {
|
|
||||||
return Err((
|
fn inner_rgb_3_arg(
|
||||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
name: &'static str,
|
||||||
args.span(),
|
mut args: ArgumentResult,
|
||||||
)
|
visitor: &mut Visitor,
|
||||||
.into())
|
) -> SassResult<Value> {
|
||||||
}
|
let alpha = if args.len() > 3 {
|
||||||
};
|
args.get(3, "alpha")
|
||||||
Ok(Value::Color(Box::new(color.with_alpha(alpha))))
|
|
||||||
} else {
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let red = args.get_err(0, "red")?;
|
let red = args.get_err(0, "red")?;
|
||||||
let green = args.get_err(1, "green")?;
|
let green = args.get_err(1, "green")?;
|
||||||
let blue = args.get_err(2, "blue")?;
|
let blue = args.get_err(2, "blue")?;
|
||||||
let alpha = args.default_arg(
|
|
||||||
3,
|
|
||||||
"alpha",
|
|
||||||
Value::Dimension(Some(Number::one()), Unit::None, true),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if [&red, &green, &blue, &alpha]
|
if red.is_special_function()
|
||||||
.iter()
|
|| green.is_special_function()
|
||||||
.copied()
|
|| blue.is_special_function()
|
||||||
.any(Value::is_special_function)
|
|| alpha
|
||||||
|
.as_ref()
|
||||||
|
.map(|alpha| alpha.node.is_special_function())
|
||||||
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
return Ok(Value::String(
|
let fn_string = if alpha.is_some() {
|
||||||
format!(
|
function_string(
|
||||||
"{}({})",
|
|
||||||
name,
|
name,
|
||||||
Value::List(
|
&[red, green, blue, alpha.unwrap().node],
|
||||||
if len == 4 {
|
visitor,
|
||||||
vec![red, green, blue, alpha]
|
args.span(),
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
vec![red, green, blue]
|
function_string(name, &[red, green, blue], visitor, args.span())?
|
||||||
},
|
};
|
||||||
ListSeparator::Comma,
|
|
||||||
Brackets::None
|
return Ok(Value::String(fn_string, QuoteKind::None));
|
||||||
)
|
|
||||||
.to_css_string(args.span(), false)?
|
|
||||||
),
|
|
||||||
QuoteKind::None,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let red = match red {
|
let span = args.span();
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
let red = red.assert_number_with_name("red", span)?;
|
||||||
(n / Number::from(100)) * Number::from(255)
|
let green = green.assert_number_with_name("green", span)?;
|
||||||
}
|
let blue = blue.assert_number_with_name("blue", span)?;
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v @ Value::Dimension(..) => {
|
Ok(Value::Color(Box::new(Color::from_rgba_fn(
|
||||||
return Err((
|
Number(fuzzy_round(percentage_or_unitless(
|
||||||
format!(
|
&red, 255.0, "red", span, visitor,
|
||||||
"$red: Expected {} to have no units or \"%\".",
|
)?)),
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
Number(fuzzy_round(percentage_or_unitless(
|
||||||
|
&green, 255.0, "green", span, visitor,
|
||||||
|
)?)),
|
||||||
|
Number(fuzzy_round(percentage_or_unitless(
|
||||||
|
&blue, 255.0, "blue", span, visitor,
|
||||||
|
)?)),
|
||||||
|
Number(
|
||||||
|
alpha
|
||||||
|
.map(|alpha| {
|
||||||
|
percentage_or_unitless(
|
||||||
|
&alpha.node.assert_number_with_name("alpha", span)?,
|
||||||
|
1.0,
|
||||||
|
"alpha",
|
||||||
|
span,
|
||||||
|
visitor,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(1.0),
|
||||||
),
|
),
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$red: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let green = match green {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
|
||||||
(n / Number::from(100)) * Number::from(255)
|
|
||||||
}
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$green: Expected {} to have no units or \"%\".",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$green: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let blue = match blue {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => {
|
|
||||||
(n / Number::from(100)) * Number::from(255)
|
|
||||||
}
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$blue: Expected {} to have no units or \"%\".",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$blue: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let alpha = match alpha {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
|
|
||||||
Value::Dimension(None, ..) => todo!(),
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$alpha: Expected {} to have no units or \"%\".",
|
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Value::Color(Box::new(Color::from_rgba(
|
|
||||||
red, green, blue, alpha,
|
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn percentage_or_unitless(
|
||||||
|
number: &SassNumber,
|
||||||
|
max: f64,
|
||||||
|
name: &str,
|
||||||
|
span: Span,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<f64> {
|
||||||
|
let value = if number.unit == Unit::None {
|
||||||
|
number.num
|
||||||
|
} else if number.unit == Unit::Percent {
|
||||||
|
(number.num * Number(max)) / Number(100.0)
|
||||||
|
} else {
|
||||||
|
return Err((
|
||||||
|
format!(
|
||||||
|
"${name}: Expected {} to have no units or \"%\".",
|
||||||
|
inspect_number(number, visitor.options, span)?
|
||||||
|
),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(value.clamp(0.0, max).0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn rgb(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
#[derive(Debug, Clone)]
|
||||||
inner_rgb("rgb", args, parser)
|
pub(crate) enum ParsedChannels {
|
||||||
|
String(String),
|
||||||
|
List(Vec<Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn rgba(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn is_var_slash(value: &Value) -> bool {
|
||||||
inner_rgb("rgba", args, parser)
|
match value {
|
||||||
|
Value::String(text, QuoteKind::Quoted) => {
|
||||||
|
text.to_ascii_lowercase().starts_with("var(") && text.contains('/')
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn parse_channels(
|
||||||
|
name: &'static str,
|
||||||
|
arg_names: &[&'static str],
|
||||||
|
mut channels: Value,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
span: Span,
|
||||||
|
) -> SassResult<ParsedChannels> {
|
||||||
|
if channels.is_var() {
|
||||||
|
let fn_string = function_string(name, &[channels], visitor, span)?;
|
||||||
|
return Ok(ParsedChannels::String(fn_string));
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_channels = channels.clone();
|
||||||
|
|
||||||
|
let mut alpha_from_slash_list = None;
|
||||||
|
|
||||||
|
if channels.separator() == ListSeparator::Slash {
|
||||||
|
let list = channels.clone().as_list();
|
||||||
|
if list.len() != 2 {
|
||||||
|
return Err((
|
||||||
|
format!(
|
||||||
|
"Only 2 slash-separated elements allowed, but {} {} passed.",
|
||||||
|
list.len(),
|
||||||
|
if list.len() == 1 { "was" } else { "were" }
|
||||||
|
),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = list[0].clone();
|
||||||
|
let inner_alpha_from_slash_list = list[1].clone();
|
||||||
|
|
||||||
|
if !alpha_from_slash_list
|
||||||
|
.as_ref()
|
||||||
|
.map(Value::is_special_function)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
inner_alpha_from_slash_list
|
||||||
|
.clone()
|
||||||
|
.assert_number_with_name("alpha", span)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha_from_slash_list = Some(inner_alpha_from_slash_list);
|
||||||
|
|
||||||
|
if list[0].is_var() {
|
||||||
|
let fn_string = function_string(name, &[original_channels], visitor, span)?;
|
||||||
|
return Ok(ParsedChannels::String(fn_string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_comma_separated = channels.separator() == ListSeparator::Comma;
|
||||||
|
let is_bracketed = matches!(channels, Value::List(_, _, Brackets::Bracketed));
|
||||||
|
|
||||||
|
if is_comma_separated || is_bracketed {
|
||||||
|
let mut err_buffer = "$channels must be".to_owned();
|
||||||
|
|
||||||
|
if is_bracketed {
|
||||||
|
err_buffer.push_str(" an unbracketed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_comma_separated {
|
||||||
|
if is_bracketed {
|
||||||
|
err_buffer.push(',');
|
||||||
|
} else {
|
||||||
|
err_buffer.push_str(" a");
|
||||||
|
}
|
||||||
|
|
||||||
|
err_buffer.push_str(" space-separated");
|
||||||
|
}
|
||||||
|
|
||||||
|
err_buffer.push_str(" list.");
|
||||||
|
|
||||||
|
return Err((err_buffer, span).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut list = channels.clone().as_list();
|
||||||
|
|
||||||
|
if list.len() > 3 {
|
||||||
|
return Err((
|
||||||
|
format!("Only 3 elements allowed, but {} were passed.", list.len()),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
} else if list.len() < 3 {
|
||||||
|
if list.iter().any(Value::is_var)
|
||||||
|
|| (!list.is_empty() && is_var_slash(list.last().unwrap()))
|
||||||
|
{
|
||||||
|
let fn_string = function_string(name, &[original_channels], visitor, span)?;
|
||||||
|
return Ok(ParsedChannels::String(fn_string));
|
||||||
|
} else {
|
||||||
|
let argument = arg_names[list.len()];
|
||||||
|
return Err((format!("Missing element ${argument}."), span).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alpha_from_slash_list) = alpha_from_slash_list {
|
||||||
|
list.push(alpha_from_slash_list);
|
||||||
|
return Ok(ParsedChannels::List(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::collapsible_match)]
|
||||||
|
match &list[2] {
|
||||||
|
Value::Dimension(SassNumber { as_slash, .. }) => match as_slash {
|
||||||
|
Some(slash) => Ok(ParsedChannels::List(vec![
|
||||||
|
list[0].clone(),
|
||||||
|
list[1].clone(),
|
||||||
|
// todo: superfluous clones
|
||||||
|
Value::Dimension(slash.0.clone()),
|
||||||
|
Value::Dimension(slash.1.clone()),
|
||||||
|
])),
|
||||||
|
None => Ok(ParsedChannels::List(list)),
|
||||||
|
},
|
||||||
|
Value::String(text, QuoteKind::None) if text.contains('/') => {
|
||||||
|
let fn_string = function_string(name, &[channels], visitor, span)?;
|
||||||
|
Ok(ParsedChannels::String(fn_string))
|
||||||
|
}
|
||||||
|
_ => Ok(ParsedChannels::List(list)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// name: Either `rgb` or `rgba` depending on the caller
|
||||||
|
fn inner_rgb(
|
||||||
|
name: &'static str,
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
|
args.max_args(4)?;
|
||||||
|
|
||||||
|
match args.len() {
|
||||||
|
0 | 1 => {
|
||||||
|
match parse_channels(
|
||||||
|
name,
|
||||||
|
&["red", "green", "blue"],
|
||||||
|
args.get_err(0, "channels")?,
|
||||||
|
visitor,
|
||||||
|
args.span(),
|
||||||
|
)? {
|
||||||
|
ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)),
|
||||||
|
ParsedChannels::List(list) => {
|
||||||
|
let args = ArgumentResult {
|
||||||
|
positional: list,
|
||||||
|
named: BTreeMap::new(),
|
||||||
|
separator: ListSeparator::Comma,
|
||||||
|
span: args.span(),
|
||||||
|
touched: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
inner_rgb_3_arg(name, args, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => inner_rgb_2_arg(name, args, visitor),
|
||||||
|
_ => inner_rgb_3_arg(name, args, visitor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
inner_rgb("rgb", args, visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
inner_rgb("rgba", args, visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.red()), Unit::None, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.red()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -371,10 +368,14 @@ pub(crate) fn red(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.green()), Unit::None, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.green()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -383,10 +384,14 @@ pub(crate) fn green(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "color")? {
|
match args.get_err(0, "color")? {
|
||||||
Value::Color(c) => Ok(Value::Dimension(Some(c.blue()), Unit::None, true)),
|
Value::Color(c) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (c.blue()),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
format!("$color: {} is not a color.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -395,7 +400,7 @@ pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let color1 = match args.get_err(0, "color1")? {
|
let color1 = match args.get_err(0, "color1")? {
|
||||||
Value::Color(c) => c,
|
Value::Color(c) => c,
|
||||||
@ -422,15 +427,23 @@ pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
let weight = match args.default_arg(
|
let weight = match args.default_arg(
|
||||||
2,
|
2,
|
||||||
"weight",
|
"weight",
|
||||||
Value::Dimension(Some(Number::from(50)), Unit::None, true),
|
Value::Dimension(SassNumber {
|
||||||
)? {
|
num: (Number::from(50)),
|
||||||
Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
|
unit: Unit::None,
|
||||||
Value::Dimension(None, ..) => todo!(),
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$weight: {} is not a number.",
|
"$weight: {} is not a number.",
|
||||||
v.to_css_string(args.span(), parser.options.is_compressed())?
|
v.to_css_string(args.span(), visitor.options.is_compressed())?
|
||||||
),
|
),
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
|
@ -1,33 +1,24 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use num_traits::{Signed, ToPrimitive, Zero};
|
pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs,
|
|
||||||
common::{Brackets, ListSeparator, QuoteKind},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(Value::Dimension(
|
Ok(Value::Dimension(SassNumber {
|
||||||
Some(Number::from(args.get_err(0, "list")?.as_list().len())),
|
num: (Number::from(args.get_err(0, "list")?.as_list().len())),
|
||||||
Unit::None,
|
unit: Unit::None,
|
||||||
true,
|
as_slash: None,
|
||||||
))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let mut list = args.get_err(0, "list")?.as_list();
|
let mut list = args.get_err(0, "list")?.as_list();
|
||||||
let (n, unit) = match args.get_err(1, "n")? {
|
let (n, unit) = match args.get_err(1, "n")? {
|
||||||
Value::Dimension(Some(num), unit, ..) => (num, unit),
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(None, u, ..) => {
|
num: n, unit: u, ..
|
||||||
|
}) if n.is_nan() => {
|
||||||
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
||||||
}
|
}
|
||||||
|
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -54,18 +45,16 @@ pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.is_decimal() {
|
|
||||||
return Err((format!("$n: {} is not an int.", n.inspect()), args.span()).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(list.remove(if n.is_positive() {
|
Ok(list.remove(if n.is_positive() {
|
||||||
n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1
|
let index = n.assert_int_with_name("n", args.span())? - 1;
|
||||||
|
debug_assert!(index > -1);
|
||||||
|
index as usize
|
||||||
} else {
|
} else {
|
||||||
list.len() - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX)
|
list.len() - n.abs().assert_int_with_name("n", args.span())? as usize
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(Value::String(
|
Ok(Value::String(
|
||||||
match args.get_err(0, "list")? {
|
match args.get_err(0, "list")? {
|
||||||
@ -78,12 +67,12 @@ pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
||||||
Value::List(v, sep, b) => (v, sep, b),
|
Value::List(v, sep, b) => (v, sep, b),
|
||||||
Value::ArgList(v) => (
|
Value::ArgList(v) => (
|
||||||
v.into_iter().map(|val| val.node).collect(),
|
v.elems.into_iter().collect(),
|
||||||
ListSeparator::Comma,
|
ListSeparator::Comma,
|
||||||
Brackets::None,
|
Brackets::None,
|
||||||
),
|
),
|
||||||
@ -91,10 +80,12 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
v => (vec![v], ListSeparator::Space, Brackets::None),
|
v => (vec![v], ListSeparator::Space, Brackets::None),
|
||||||
};
|
};
|
||||||
let (n, unit) = match args.get_err(1, "n")? {
|
let (n, unit) = match args.get_err(1, "n")? {
|
||||||
Value::Dimension(Some(num), unit, ..) => (num, unit),
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(None, u, ..) => {
|
num: n, unit: u, ..
|
||||||
|
}) if n.is_nan() => {
|
||||||
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
return Err((format!("$n: NaN{} is not an int.", u), args.span()).into())
|
||||||
}
|
}
|
||||||
|
Value::Dimension(SassNumber { num, unit, .. }) => (num, unit),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
format!("$n: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -130,15 +121,15 @@ pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
let val = args.get_err(2, "value")?;
|
let val = args.get_err(2, "value")?;
|
||||||
|
|
||||||
if n.is_positive() {
|
if n.is_positive() {
|
||||||
list[n.to_integer().to_usize().unwrap_or(std::usize::MAX) - 1] = val;
|
list[n.assert_int_with_name("n", args.span())? as usize - 1] = val;
|
||||||
} else {
|
} else {
|
||||||
list[len - n.abs().to_integer().to_usize().unwrap_or(std::usize::MAX)] = val;
|
list[len - n.abs().assert_int_with_name("n", args.span())? as usize] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::List(list, sep, brackets))
|
Ok(Value::List(list, sep, brackets))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
let (mut list, sep, brackets) = match args.get_err(0, "list")? {
|
||||||
Value::List(v, sep, b) => (v, sep, b),
|
Value::List(v, sep, b) => (v, sep, b),
|
||||||
@ -149,14 +140,15 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
|||||||
2,
|
2,
|
||||||
"separator",
|
"separator",
|
||||||
Value::String("auto".to_owned(), QuoteKind::None),
|
Value::String("auto".to_owned(), QuoteKind::None),
|
||||||
)? {
|
) {
|
||||||
Value::String(s, ..) => match s.as_str() {
|
Value::String(s, ..) => match s.as_str() {
|
||||||
"auto" => sep,
|
"auto" => sep,
|
||||||
"comma" => ListSeparator::Comma,
|
"comma" => ListSeparator::Comma,
|
||||||
"space" => ListSeparator::Space,
|
"space" => ListSeparator::Space,
|
||||||
|
"slash" => ListSeparator::Slash,
|
||||||
_ => {
|
_ => {
|
||||||
return Err((
|
return Err((
|
||||||
"$separator: Must be \"space\", \"comma\", or \"auto\".",
|
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
@ -176,7 +168,7 @@ pub(crate) fn append(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
|||||||
Ok(Value::List(list, sep, brackets))
|
Ok(Value::List(list, sep, brackets))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(4)?;
|
args.max_args(4)?;
|
||||||
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
|
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
|
||||||
Value::List(v, sep, brackets) => (v, sep, brackets),
|
Value::List(v, sep, brackets) => (v, sep, brackets),
|
||||||
@ -192,7 +184,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
2,
|
2,
|
||||||
"separator",
|
"separator",
|
||||||
Value::String("auto".to_owned(), QuoteKind::None),
|
Value::String("auto".to_owned(), QuoteKind::None),
|
||||||
)? {
|
) {
|
||||||
Value::String(s, ..) => match s.as_str() {
|
Value::String(s, ..) => match s.as_str() {
|
||||||
"auto" => {
|
"auto" => {
|
||||||
if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) {
|
if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) {
|
||||||
@ -203,9 +195,10 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
"comma" => ListSeparator::Comma,
|
"comma" => ListSeparator::Comma,
|
||||||
"space" => ListSeparator::Space,
|
"space" => ListSeparator::Space,
|
||||||
|
"slash" => ListSeparator::Slash,
|
||||||
_ => {
|
_ => {
|
||||||
return Err((
|
return Err((
|
||||||
"$separator: Must be \"space\", \"comma\", or \"auto\".",
|
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
@ -224,7 +217,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
3,
|
3,
|
||||||
"bracketed",
|
"bracketed",
|
||||||
Value::String("auto".to_owned(), QuoteKind::None),
|
Value::String("auto".to_owned(), QuoteKind::None),
|
||||||
)? {
|
) {
|
||||||
Value::String(s, ..) => match s.as_str() {
|
Value::String(s, ..) => match s.as_str() {
|
||||||
"auto" => brackets,
|
"auto" => brackets,
|
||||||
_ => Brackets::Bracketed,
|
_ => Brackets::Bracketed,
|
||||||
@ -243,7 +236,7 @@ pub(crate) fn join(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
Ok(Value::List(list1, sep, brackets))
|
Ok(Value::List(list1, sep, brackets))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(Value::bool(match args.get_err(0, "list")? {
|
Ok(Value::bool(match args.get_err(0, "list")? {
|
||||||
Value::List(.., brackets) => match brackets {
|
Value::List(.., brackets) => match brackets {
|
||||||
@ -254,7 +247,7 @@ pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let list = args.get_err(0, "list")?.as_list();
|
let list = args.get_err(0, "list")?.as_list();
|
||||||
let value = args.get_err(1, "value")?;
|
let value = args.get_err(1, "value")?;
|
||||||
@ -262,10 +255,14 @@ pub(crate) fn index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
Some(v) => Number::from(v + 1),
|
Some(v) => Number::from(v + 1),
|
||||||
None => return Ok(Value::Null),
|
None => return Ok(Value::Null),
|
||||||
};
|
};
|
||||||
Ok(Value::Dimension(Some(index), Unit::None, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (index),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn zip(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let lists = args
|
let lists = args
|
||||||
.get_variadic()?
|
.get_variadic()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1,26 +1,6 @@
|
|||||||
macro_rules! bound {
|
macro_rules! bound {
|
||||||
($args:ident, $name:literal, $arg:ident, $unit:ident, $low:literal, $high:literal) => {
|
($args:ident, $name:literal, $arg:expr, $unit:expr, $low:literal, $high:literal) => {
|
||||||
if $arg > Number::from($high) || $arg < Number::from($low) {
|
if !($arg <= Number::from($high) && $arg >= Number::from($low)) {
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"${}: Expected {}{} to be within {}{} and {}{}.",
|
|
||||||
$name,
|
|
||||||
$arg.inspect(),
|
|
||||||
$unit,
|
|
||||||
$low,
|
|
||||||
$unit,
|
|
||||||
$high,
|
|
||||||
$unit,
|
|
||||||
),
|
|
||||||
$args.span(),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
} else {
|
|
||||||
$arg
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($args:ident, $name:literal, $arg:ident, $unit:path, $low:literal, $high:literal) => {
|
|
||||||
if $arg > Number::from($high) || $arg < Number::from($low) {
|
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"${}: Expected {}{} to be within {}{} and {}{}.",
|
"${}: Expected {}{} to be within {}{} and {}{}.",
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use crate::{
|
pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args::CallArgs,
|
|
||||||
common::{Brackets, ListSeparator},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
value::{SassMap, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let key = args.get_err(1, "key")?;
|
let key = args.get_err(1, "key")?;
|
||||||
let map = match args.get_err(0, "map")? {
|
let map = match args.get_err(0, "map")? {
|
||||||
@ -26,7 +18,7 @@ pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
Ok(map.get(&key).unwrap_or(Value::Null))
|
Ok(map.get(&key).unwrap_or(Value::Null))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let key = args.get_err(1, "key")?;
|
let key = args.get_err(1, "key")?;
|
||||||
let map = match args.get_err(0, "map")? {
|
let map = match args.get_err(0, "map")? {
|
||||||
@ -44,7 +36,7 @@ pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
Ok(Value::bool(map.get(&key).is_some()))
|
Ok(Value::bool(map.get(&key).is_some()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let map = match args.get_err(0, "map")? {
|
let map = match args.get_err(0, "map")? {
|
||||||
Value::Map(m) => m,
|
Value::Map(m) => m,
|
||||||
@ -65,7 +57,7 @@ pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let map = match args.get_err(0, "map")? {
|
let map = match args.get_err(0, "map")? {
|
||||||
Value::Map(m) => m,
|
Value::Map(m) => m,
|
||||||
@ -86,7 +78,7 @@ pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
return Err(("Expected $args to contain a key.", args.span()).into());
|
return Err(("Expected $args to contain a key.", args.span()).into());
|
||||||
}
|
}
|
||||||
@ -150,10 +142,10 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
while let Some((key, queued_map)) = map_queue.pop() {
|
while let Some((key, queued_map)) = map_queue.pop() {
|
||||||
match map_queue.last_mut() {
|
match map_queue.last_mut() {
|
||||||
Some((_, map)) => {
|
Some((_, map)) => {
|
||||||
map.insert(key.node, Value::Map(queued_map));
|
map.insert(key, Value::Map(queued_map));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
map1.insert(key.node, Value::Map(queued_map));
|
map1.insert(key, Value::Map(queued_map));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +155,7 @@ pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
Ok(Value::Map(map1))
|
Ok(Value::Map(map1))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let mut map = match args.get_err(0, "map")? {
|
let mut map = match args.get_err(0, "map")? {
|
||||||
Value::Map(m) => m,
|
Value::Map(m) => m,
|
||||||
Value::List(v, ..) if v.is_empty() => SassMap::new(),
|
Value::List(v, ..) if v.is_empty() => SassMap::new(),
|
||||||
@ -183,7 +175,7 @@ pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
Ok(Value::Map(map))
|
Ok(Value::Map(map))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let key_position = args.len().saturating_sub(2);
|
let key_position = args.len().saturating_sub(2);
|
||||||
let value_position = args.len().saturating_sub(1);
|
let value_position = args.len().saturating_sub(1);
|
||||||
|
|
||||||
@ -200,7 +192,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = args.get_err(key_position, "key")?;
|
let key = Spanned {
|
||||||
|
node: args.get_err(key_position, "key")?,
|
||||||
|
span: args.span(),
|
||||||
|
};
|
||||||
let value = args.get_err(value_position, "value")?;
|
let value = args.get_err(value_position, "value")?;
|
||||||
|
|
||||||
let keys = args.get_variadic()?;
|
let keys = args.get_variadic()?;
|
||||||
@ -232,10 +227,10 @@ pub(crate) fn map_set(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
while let Some((key, queued_map)) = map_queue.pop() {
|
while let Some((key, queued_map)) = map_queue.pop() {
|
||||||
match map_queue.last_mut() {
|
match map_queue.last_mut() {
|
||||||
Some((_, next_map)) => {
|
Some((_, next_map)) => {
|
||||||
next_map.insert(key.node, Value::Map(queued_map));
|
next_map.insert(key, Value::Map(queued_map));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
map.insert(key.node, Value::Map(queued_map));
|
map.insert(key, Value::Map(queued_map));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,14 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::{builtin::builtin_imports::*, evaluate::div};
|
||||||
|
|
||||||
#[cfg(feature = "random")]
|
pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
use num_traits::{One, Signed, ToPrimitive, Zero};
|
|
||||||
#[cfg(feature = "random")]
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs,
|
|
||||||
common::Op,
|
|
||||||
error::SassResult,
|
|
||||||
parse::{HigherIntermediateValue, Parser, ValueVisitor},
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let num = match args.get_err(0, "number")? {
|
let num = match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), Unit::None, _) => Some(n * Number::from(100)),
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(None, Unit::None, _) => None,
|
num: n,
|
||||||
v @ Value::Dimension(..) => {
|
unit: Unit::None,
|
||||||
|
as_slash: _,
|
||||||
|
}) => n * Number::from(100),
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to have no units.",
|
"$number: Expected {} to have no units.",
|
||||||
@ -37,14 +26,29 @@ pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(Value::Dimension(num, Unit::Percent, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num,
|
||||||
|
unit: Unit::Percent,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "number")? {
|
match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.round()), u, true)),
|
// todo: better error message, consider finities
|
||||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||||
|
Err(("Infinity or NaN toInt", args.span()).into())
|
||||||
|
}
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (n.round()),
|
||||||
|
unit: u,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -53,11 +57,22 @@ pub(crate) fn round(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "number")? {
|
match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.ceil()), u, true)),
|
// todo: better error message, consider finities
|
||||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||||
|
Err(("Infinity or NaN toInt", args.span()).into())
|
||||||
|
}
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (n.ceil()),
|
||||||
|
unit: u,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -66,11 +81,22 @@ pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "number")? {
|
match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.floor()), u, true)),
|
// todo: better error message, consider finities
|
||||||
Value::Dimension(None, ..) => Err(("Infinity or NaN toInt", args.span()).into()),
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => {
|
||||||
|
Err(("Infinity or NaN toInt", args.span()).into())
|
||||||
|
}
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (n.floor()),
|
||||||
|
unit: u,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -79,11 +105,18 @@ pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "number")? {
|
match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.abs()), u, true)),
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(None, u, ..) => Ok(Value::Dimension(None, u, true)),
|
num: n,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (n.abs()),
|
||||||
|
unit: u,
|
||||||
|
as_slash: None,
|
||||||
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -92,10 +125,14 @@ pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let unit1 = match args.get_err(0, "number1")? {
|
let unit1 = match args.get_err(0, "number1")? {
|
||||||
Value::Dimension(_, u, _) => u,
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number1: {} is not a number.", v.inspect(args.span())?),
|
format!("$number1: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -105,7 +142,11 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let unit2 = match args.get_err(1, "number2")? {
|
let unit2 = match args.get_err(1, "number2")? {
|
||||||
Value::Dimension(_, u, _) => u,
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number2: {} is not a number.", v.inspect(args.span())?),
|
format!("$number2: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -120,40 +161,28 @@ pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
|
|
||||||
// TODO: write tests for this
|
// TODO: write tests for this
|
||||||
#[cfg(feature = "random")]
|
#[cfg(feature = "random")]
|
||||||
pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let limit = match args.default_arg(0, "limit", Value::Null)? {
|
let limit = args.default_arg(0, "limit", Value::Null);
|
||||||
Value::Dimension(Some(n), ..) => n,
|
|
||||||
Value::Dimension(None, u, ..) => {
|
if matches!(limit, Value::Null) {
|
||||||
return Err((format!("$limit: NaN{} is not an int.", u), args.span()).into())
|
|
||||||
}
|
|
||||||
Value::Null => {
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
return Ok(Value::Dimension(
|
return Ok(Value::Dimension(SassNumber {
|
||||||
Some(Number::from(rng.gen_range(0.0..1.0))),
|
num: (Number::from(rng.gen_range(0.0..1.0))),
|
||||||
Unit::None,
|
unit: Unit::None,
|
||||||
true,
|
as_slash: None,
|
||||||
));
|
}));
|
||||||
}
|
}
|
||||||
v => {
|
|
||||||
return Err((
|
let limit = limit.assert_number_with_name("limit", args.span())?.num();
|
||||||
format!("$limit: {} is not a number.", v.inspect(args.span())?),
|
let limit_int = limit.assert_int_with_name("limit", args.span())?;
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if limit.is_one() {
|
if limit.is_one() {
|
||||||
return Ok(Value::Dimension(Some(Number::one()), Unit::None, true));
|
return Ok(Value::Dimension(SassNumber {
|
||||||
}
|
num: (Number::one()),
|
||||||
|
unit: Unit::None,
|
||||||
if limit.is_decimal() {
|
as_slash: None,
|
||||||
return Err((
|
}));
|
||||||
format!("$limit: {} is not an int.", limit.inspect()),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit.is_zero() || limit.is_negative() {
|
if limit.is_zero() || limit.is_negative() {
|
||||||
@ -164,134 +193,112 @@ pub(crate) fn random(mut args: CallArgs, parser: &mut Parser) -> SassResult<Valu
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = match limit.to_integer().to_u32() {
|
|
||||||
Some(n) => n,
|
|
||||||
None => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"max must be in range 0 < max \u{2264} 2^32, was {}",
|
|
||||||
limit.inspect()
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
Ok(Value::Dimension(
|
Ok(Value::Dimension(SassNumber {
|
||||||
Some(Number::from(rng.gen_range(0..limit) + 1)),
|
num: (Number::from(rng.gen_range(0..limit_int) + 1)),
|
||||||
Unit::None,
|
unit: Unit::None,
|
||||||
true,
|
as_slash: None,
|
||||||
))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn min(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.min_args(1)?;
|
args.min_args(1)?;
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
let mut nums = args
|
let mut nums = args
|
||||||
.get_variadic()?
|
.get_variadic()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|val| match val.node {
|
.map(|val| match val.node {
|
||||||
Value::Dimension(number, unit, _) => Ok((number, unit)),
|
Value::Dimension(SassNumber {
|
||||||
|
num: number,
|
||||||
|
unit,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok((number, unit)),
|
||||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||||
})
|
})
|
||||||
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
|
.collect::<SassResult<Vec<(Number, Unit)>>>()?
|
||||||
.into_iter();
|
.into_iter();
|
||||||
|
|
||||||
let mut min = match nums.next() {
|
let mut min = match nums.next() {
|
||||||
Some((Some(n), u)) => (n, u),
|
Some((n, u)) => (n, u),
|
||||||
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
|
|
||||||
None => unreachable!(),
|
None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (num, unit) in nums {
|
for (num, unit) in nums {
|
||||||
let num = match num {
|
let lhs = Value::Dimension(SassNumber {
|
||||||
Some(n) => n,
|
num,
|
||||||
None => continue,
|
unit: unit.clone(),
|
||||||
};
|
as_slash: None,
|
||||||
|
});
|
||||||
|
let rhs = Value::Dimension(SassNumber {
|
||||||
|
num: (min.0),
|
||||||
|
unit: min.1.clone(),
|
||||||
|
as_slash: None,
|
||||||
|
});
|
||||||
|
|
||||||
if ValueVisitor::new(parser, span)
|
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_true() {
|
||||||
.less_than(
|
|
||||||
HigherIntermediateValue::Literal(Value::Dimension(
|
|
||||||
Some(num.clone()),
|
|
||||||
unit.clone(),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
HigherIntermediateValue::Literal(Value::Dimension(
|
|
||||||
Some(min.0.clone()),
|
|
||||||
min.1.clone(),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
)?
|
|
||||||
.is_true()
|
|
||||||
{
|
|
||||||
min = (num, unit);
|
min = (num, unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Value::Dimension(Some(min.0), min.1, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (min.0),
|
||||||
|
unit: min.1,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn max(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.min_args(1)?;
|
args.min_args(1)?;
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
let mut nums = args
|
let mut nums = args
|
||||||
.get_variadic()?
|
.get_variadic()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|val| match val.node {
|
.map(|val| match val.node {
|
||||||
Value::Dimension(number, unit, _) => Ok((number, unit)),
|
Value::Dimension(SassNumber {
|
||||||
|
num: number,
|
||||||
|
unit,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Ok((number, unit)),
|
||||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||||
})
|
})
|
||||||
.collect::<SassResult<Vec<(Option<Number>, Unit)>>>()?
|
.collect::<SassResult<Vec<(Number, Unit)>>>()?
|
||||||
.into_iter();
|
.into_iter();
|
||||||
|
|
||||||
let mut max = match nums.next() {
|
let mut max = match nums.next() {
|
||||||
Some((Some(n), u)) => (n, u),
|
Some((n, u)) => (n, u),
|
||||||
Some((None, u)) => return Ok(Value::Dimension(None, u, true)),
|
|
||||||
None => unreachable!(),
|
None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (num, unit) in nums {
|
for (num, unit) in nums {
|
||||||
let num = match num {
|
let lhs = Value::Dimension(SassNumber {
|
||||||
Some(n) => n,
|
num,
|
||||||
None => continue,
|
unit: unit.clone(),
|
||||||
};
|
as_slash: None,
|
||||||
|
});
|
||||||
|
let rhs = Value::Dimension(SassNumber {
|
||||||
|
num: (max.0),
|
||||||
|
unit: max.1.clone(),
|
||||||
|
as_slash: None,
|
||||||
|
});
|
||||||
|
|
||||||
if ValueVisitor::new(parser, span)
|
if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)?.is_true()
|
||||||
.greater_than(
|
|
||||||
HigherIntermediateValue::Literal(Value::Dimension(
|
|
||||||
Some(num.clone()),
|
|
||||||
unit.clone(),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
HigherIntermediateValue::Literal(Value::Dimension(
|
|
||||||
Some(max.0.clone()),
|
|
||||||
max.1.clone(),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
)?
|
|
||||||
.is_true()
|
|
||||||
{
|
{
|
||||||
max = (num, unit);
|
max = (num, unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Value::Dimension(Some(max.0), max.1, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (max.0),
|
||||||
|
unit: max.1,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn divide(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let number1 = args.get_err(0, "number1")?;
|
let number1 = args.get_err(0, "number1")?;
|
||||||
let number2 = args.get_err(1, "number2")?;
|
let number2 = args.get_err(1, "number2")?;
|
||||||
|
|
||||||
ValueVisitor::new(parser, args.span()).eval(
|
div(number1, number2, visitor.options, args.span())
|
||||||
HigherIntermediateValue::BinaryOp(
|
|
||||||
Box::new(HigherIntermediateValue::Literal(number1)),
|
|
||||||
Op::Div,
|
|
||||||
Box::new(HigherIntermediateValue::Literal(number2)),
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use codemap::Spanned;
|
// todo: this should be a constant of some sort. we shouldn't be allocating this
|
||||||
|
// every time
|
||||||
|
pub(crate) fn if_arguments() -> ArgumentDeclaration {
|
||||||
|
ArgumentDeclaration {
|
||||||
|
args: vec![
|
||||||
|
Argument {
|
||||||
|
name: Identifier::from("condition"),
|
||||||
|
default: None,
|
||||||
|
},
|
||||||
|
Argument {
|
||||||
|
name: Identifier::from("if-true"),
|
||||||
|
default: None,
|
||||||
|
},
|
||||||
|
Argument {
|
||||||
|
name: Identifier::from("if-false"),
|
||||||
|
default: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rest: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use crate::{
|
fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args::CallArgs,
|
|
||||||
common::{Identifier, QuoteKind},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{SassFunction, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
if args.get_err(0, "condition")?.is_true() {
|
if args.get_err(0, "condition")?.is_true() {
|
||||||
Ok(args.get_err(1, "if-true")?)
|
Ok(args.get_err(1, "if-true")?)
|
||||||
@ -20,7 +31,7 @@ fn if_(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "feature")? {
|
match args.get_err(0, "feature")? {
|
||||||
#[allow(clippy::match_same_arms)]
|
#[allow(clippy::match_same_arms)]
|
||||||
@ -39,7 +50,7 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
|||||||
// The "Custom Properties Level 1" spec is supported. This means
|
// The "Custom Properties Level 1" spec is supported. This means
|
||||||
// that custom properties are parsed statically, with only
|
// that custom properties are parsed statically, with only
|
||||||
// interpolation treated as SassScript.
|
// interpolation treated as SassScript.
|
||||||
"custom-property" => Value::False,
|
"custom-property" => Value::True,
|
||||||
_ => Value::False,
|
_ => Value::False,
|
||||||
}),
|
}),
|
||||||
v => Err((
|
v => Err((
|
||||||
@ -50,10 +61,14 @@ pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let unit = match args.get_err(0, "number")? {
|
let unit = match args.get_err(0, "number")? {
|
||||||
Value::Dimension(_, u, _) => u.to_string(),
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u.to_string(),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -65,17 +80,21 @@ pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value>
|
|||||||
Ok(Value::String(unit, QuoteKind::Quoted))
|
Ok(Value::String(unit, QuoteKind::Quoted))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let value = args.get_err(0, "value")?;
|
let value = args.get_err(0, "value")?;
|
||||||
Ok(Value::String(value.kind().to_owned(), QuoteKind::None))
|
Ok(Value::String(value.kind().to_owned(), QuoteKind::None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(match args.get_err(0, "number")? {
|
Ok(match args.get_err(0, "number")? {
|
||||||
Value::Dimension(_, Unit::None, _) => Value::True,
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(..) => Value::False,
|
num: _,
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: _,
|
||||||
|
}) => Value::True,
|
||||||
|
Value::Dimension(SassNumber { .. }) => Value::False,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -86,7 +105,7 @@ pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser) -> SassResult<Va
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(Value::String(
|
Ok(Value::String(
|
||||||
args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
|
args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
|
||||||
@ -94,12 +113,13 @@ pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn variable_exists(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "name")? {
|
match args.get_err(0, "name")? {
|
||||||
Value::String(s, _) => Ok(Value::bool(
|
Value::String(s, _) => Ok(Value::bool(visitor.env.var_exists(s.into(), None)?)),
|
||||||
parser.scopes.var_exists(s.into(), parser.global_scope),
|
|
||||||
)),
|
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$name: {} is not a string.", v.inspect(args.span())?),
|
format!("$name: {} is not a string.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -108,7 +128,10 @@ pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn global_variable_exists(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let name: Identifier = match args.get_err(0, "name")? {
|
let name: Identifier = match args.get_err(0, "name")? {
|
||||||
@ -122,7 +145,7 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
let module = match args.default_arg(1, "module", Value::Null) {
|
||||||
Value::String(s, _) => Some(s),
|
Value::String(s, _) => Some(s),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
@ -135,16 +158,17 @@ pub(crate) fn global_variable_exists(mut args: CallArgs, parser: &mut Parser) ->
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::bool(if let Some(module_name) = module {
|
Ok(Value::bool(if let Some(module_name) = module {
|
||||||
parser
|
(*(*visitor.env.modules)
|
||||||
.modules
|
.borrow()
|
||||||
.get(module_name.into(), args.span())?
|
.get(module_name.into(), args.span())?)
|
||||||
|
.borrow()
|
||||||
.var_exists(name)
|
.var_exists(name)
|
||||||
} else {
|
} else {
|
||||||
parser.global_scope.var_exists(name)
|
(*visitor.env.global_vars()).borrow().contains_key(&name)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let name: Identifier = match args.get_err(0, "name")? {
|
let name: Identifier = match args.get_err(0, "name")? {
|
||||||
Value::String(s, _) => s.into(),
|
Value::String(s, _) => s.into(),
|
||||||
@ -157,7 +181,7 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
let module = match args.default_arg(1, "module", Value::Null) {
|
||||||
Value::String(s, _) => Some(s),
|
Value::String(s, _) => Some(s),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
@ -170,16 +194,20 @@ pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::bool(if let Some(module_name) = module {
|
Ok(Value::bool(if let Some(module_name) = module {
|
||||||
parser
|
(*(*visitor.env.modules)
|
||||||
.modules
|
.borrow()
|
||||||
.get(module_name.into(), args.span())?
|
.get(module_name.into(), args.span())?)
|
||||||
|
.borrow()
|
||||||
.mixin_exists(name)
|
.mixin_exists(name)
|
||||||
} else {
|
} else {
|
||||||
parser.scopes.mixin_exists(name, parser.global_scope)
|
visitor.env.mixin_exists(name)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn function_exists(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let name: Identifier = match args.get_err(0, "name")? {
|
let name: Identifier = match args.get_err(0, "name")? {
|
||||||
@ -193,7 +221,7 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let module = match args.default_arg(1, "module", Value::Null)? {
|
let module = match args.default_arg(1, "module", Value::Null) {
|
||||||
Value::String(s, _) => Some(s),
|
Value::String(s, _) => Some(s),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
@ -206,16 +234,17 @@ pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser) -> SassRe
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::bool(if let Some(module_name) = module {
|
Ok(Value::bool(if let Some(module_name) = module {
|
||||||
parser
|
(*(*visitor.env.modules)
|
||||||
.modules
|
.borrow()
|
||||||
.get(module_name.into(), args.span())?
|
.get(module_name.into(), args.span())?)
|
||||||
|
.borrow()
|
||||||
.fn_exists(name)
|
.fn_exists(name)
|
||||||
} else {
|
} else {
|
||||||
parser.scopes.fn_exists(name, parser.global_scope)
|
visitor.env.fn_exists(name)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let name: Identifier = match args.get_err(0, "name")? {
|
let name: Identifier = match args.get_err(0, "name")? {
|
||||||
Value::String(s, _) => s.into(),
|
Value::String(s, _) => s.into(),
|
||||||
@ -227,8 +256,8 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let css = args.default_arg(1, "css", Value::False)?.is_true();
|
let css = args.default_arg(1, "css", Value::False).is_true();
|
||||||
let module = match args.default_arg(2, "module", Value::Null)? {
|
let module = match args.default_arg(2, "module", Value::Null) {
|
||||||
Value::String(s, ..) => Some(s),
|
Value::String(s, ..) => Some(s),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => {
|
v => {
|
||||||
@ -240,7 +269,7 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let func = match if let Some(module_name) = module {
|
let func = if let Some(module_name) = module {
|
||||||
if css {
|
if css {
|
||||||
return Err((
|
return Err((
|
||||||
"$css and $module may not both be passed at once.",
|
"$css and $module may not both be passed at once.",
|
||||||
@ -249,68 +278,90 @@ pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser) -> SassResul
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
parser
|
visitor.env.get_fn(
|
||||||
.modules
|
name,
|
||||||
.get(module_name.into(), args.span())?
|
Some(Spanned {
|
||||||
.get_fn(Spanned {
|
node: module_name.into(),
|
||||||
node: name,
|
|
||||||
span: args.span(),
|
span: args.span(),
|
||||||
})?
|
}),
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
parser.scopes.get_fn(name, parser.global_scope)
|
match visitor.env.get_fn(name, None)? {
|
||||||
} {
|
Some(f) => Some(f),
|
||||||
Some(f) => f,
|
None => GLOBAL_FUNCTIONS
|
||||||
None => match GLOBAL_FUNCTIONS.get(name.as_str()) {
|
.get(name.as_str())
|
||||||
Some(f) => SassFunction::Builtin(f.clone(), name),
|
.map(|f| SassFunction::Builtin(f.clone(), name)),
|
||||||
None => return Err((format!("Function not found: {}", name), args.span()).into()),
|
}
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::FunctionRef(func))
|
match func {
|
||||||
|
Some(func) => Ok(Value::FunctionRef(func)),
|
||||||
|
None => Err((format!("Function not found: {}", name), args.span()).into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn call(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
let span = args.span();
|
||||||
let func = match args.get_err(0, "function")? {
|
let func = match args.get_err(0, "function")? {
|
||||||
Value::FunctionRef(f) => f,
|
Value::FunctionRef(f) => f,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$function: {} is not a function reference.",
|
"$function: {} is not a function reference.",
|
||||||
v.inspect(args.span())?
|
v.inspect(span)?
|
||||||
),
|
),
|
||||||
args.span(),
|
span,
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
func.call(args.decrement(), None, parser)
|
|
||||||
|
args.remove_positional(0).unwrap();
|
||||||
|
|
||||||
|
visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub(crate) fn content_exists(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(0)?;
|
args.max_args(0)?;
|
||||||
if !parser.flags.in_mixin() {
|
if !visitor.flags.in_mixin() {
|
||||||
return Err((
|
return Err((
|
||||||
"content-exists() may only be called within a mixin.",
|
"content-exists() may only be called within a mixin.",
|
||||||
parser.span_before,
|
args.span(),
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
Ok(Value::bool(
|
Ok(Value::bool(visitor.env.content.is_some()))
|
||||||
parser.content.last().map_or(false, |c| c.content.is_some()),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables, clippy::needless_pass_by_value)]
|
pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
pub(crate) fn keywords(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
|
|
||||||
Err((
|
let span = args.span();
|
||||||
"Builtin function `keywords` is not yet implemented",
|
|
||||||
args.span(),
|
let args = match args.get_err(0, "args")? {
|
||||||
|
Value::ArgList(args) => args,
|
||||||
|
v => {
|
||||||
|
return Err((
|
||||||
|
format!("$args: {} is not an argument list.", v.inspect(span)?),
|
||||||
|
span,
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Map(SassMap::new_with(
|
||||||
|
args.into_keywords()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, val)| {
|
||||||
|
(
|
||||||
|
Value::String(name.to_string(), QuoteKind::None).span(span),
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||||
f.insert("if", Builtin::new(if_));
|
f.insert("if", Builtin::new(if_));
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{BTreeSet, HashMap},
|
||||||
sync::atomic::{AtomicUsize, Ordering},
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value};
|
use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
@ -21,16 +21,19 @@ pub mod meta;
|
|||||||
pub mod selector;
|
pub mod selector;
|
||||||
pub mod string;
|
pub mod string;
|
||||||
|
|
||||||
|
// todo: maybe Identifier instead of str?
|
||||||
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
|
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
|
||||||
|
|
||||||
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
|
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
// TODO: impl Fn
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Builtin(pub fn(CallArgs, &mut Parser) -> SassResult<Value>, usize);
|
pub(crate) struct Builtin(
|
||||||
|
pub fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
|
||||||
|
usize,
|
||||||
|
);
|
||||||
|
|
||||||
impl Builtin {
|
impl Builtin {
|
||||||
pub fn new(body: fn(CallArgs, &mut Parser) -> SassResult<Value>) -> Builtin {
|
pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>) -> Builtin {
|
||||||
let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed);
|
let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
Self(body, count)
|
Self(body, count)
|
||||||
}
|
}
|
||||||
@ -55,3 +58,24 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<GlobalFunctionMap> = Lazy::new(|| {
|
|||||||
string::declare(&mut m);
|
string::declare(&mut m);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy<BTreeSet<&str>> = Lazy::new(|| {
|
||||||
|
GLOBAL_FUNCTIONS
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.filter(|&name| {
|
||||||
|
!matches!(
|
||||||
|
name,
|
||||||
|
"rgb"
|
||||||
|
| "rgba"
|
||||||
|
| "hsl"
|
||||||
|
| "hsla"
|
||||||
|
| "grayscale"
|
||||||
|
| "invert"
|
||||||
|
| "alpha"
|
||||||
|
| "opacity"
|
||||||
|
| "saturate"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::selector::{
|
||||||
args::CallArgs,
|
ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList,
|
||||||
common::{Brackets, ListSeparator, QuoteKind},
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
selector::{ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList},
|
|
||||||
value::Value,
|
|
||||||
};
|
};
|
||||||
|
use crate::serializer::serialize_selector_list;
|
||||||
|
|
||||||
pub(crate) fn is_superselector(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn is_superselector(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let parent_selector = args
|
let parent_selector =
|
||||||
.get_err(0, "super")?
|
args.get_err(0, "super")?
|
||||||
.to_selector(parser, "super", false)?;
|
.to_selector(visitor, "super", false, args.span())?;
|
||||||
let child_selector = args.get_err(1, "sub")?.to_selector(parser, "sub", false)?;
|
let child_selector = args
|
||||||
|
.get_err(1, "sub")?
|
||||||
|
.to_selector(visitor, "sub", false, args.span())?;
|
||||||
|
|
||||||
Ok(Value::bool(
|
Ok(Value::bool(
|
||||||
parent_selector.is_super_selector(&child_selector),
|
parent_selector.is_super_selector(&child_selector),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn simple_selectors(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
// todo: Value::to_compound_selector
|
// todo: Value::to_compound_selector
|
||||||
let selector = args
|
let selector =
|
||||||
.get_err(0, "selector")?
|
args.get_err(0, "selector")?
|
||||||
.to_selector(parser, "selector", false)?;
|
.to_selector(visitor, "selector", false, args.span())?;
|
||||||
|
|
||||||
if selector.0.components.len() != 1 {
|
if selector.0.components.len() != 1 {
|
||||||
return Err(("$selector: expected selector.", args.span()).into());
|
return Err(("$selector: expected selector.", args.span()).into());
|
||||||
@ -51,16 +55,16 @@ pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser) -> SassR
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_parse(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
Ok(args
|
Ok(args
|
||||||
.get_err(0, "selector")?
|
.get_err(0, "selector")?
|
||||||
.to_selector(parser, "selector", false)
|
.to_selector(visitor, "selector", false, args.span())
|
||||||
.map_err(|_| ("$selector: expected selector.", args.span()))?
|
.map_err(|_| ("$selector: expected selector.", args.span()))?
|
||||||
.into_value())
|
.into_value())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
let selectors = args.get_variadic()?;
|
let selectors = args.get_variadic()?;
|
||||||
if selectors.is_empty() {
|
if selectors.is_empty() {
|
||||||
@ -69,7 +73,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
|
|
||||||
Ok(selectors
|
Ok(selectors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|sel| sel.node.to_selector(parser, "selectors", true))
|
.map(|sel| sel.node.to_selector(visitor, "selectors", true, span))
|
||||||
.collect::<SassResult<Vec<Selector>>>()?
|
.collect::<SassResult<Vec<Selector>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(
|
.try_fold(
|
||||||
@ -81,7 +85,7 @@ pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
.into_value())
|
.into_value())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
let selectors = args.get_variadic()?;
|
let selectors = args.get_variadic()?;
|
||||||
if selectors.is_empty() {
|
if selectors.is_empty() {
|
||||||
@ -90,7 +94,7 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
|
|
||||||
let mut parsed_selectors = selectors
|
let mut parsed_selectors = selectors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.node.to_selector(parser, "selectors", false))
|
.map(|s| s.node.to_selector(visitor, "selectors", false, span))
|
||||||
.collect::<SassResult<Vec<Selector>>>()?;
|
.collect::<SassResult<Vec<Selector>>>()?;
|
||||||
|
|
||||||
let first = parsed_selectors.remove(0);
|
let first = parsed_selectors.remove(0);
|
||||||
@ -109,7 +113,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
Some(v) => ComplexSelectorComponent::Compound(v),
|
Some(v) => ComplexSelectorComponent::Compound(v),
|
||||||
None => {
|
None => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("Can't append {} to {}.", complex, parent),
|
format!(
|
||||||
|
"Can't append {} to {}.",
|
||||||
|
complex,
|
||||||
|
serialize_selector_list(
|
||||||
|
&parent.0,
|
||||||
|
visitor.options,
|
||||||
|
span
|
||||||
|
)
|
||||||
|
),
|
||||||
span,
|
span,
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
@ -118,7 +130,15 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
components.extend(complex.components.into_iter().skip(1));
|
components.extend(complex.components.into_iter().skip(1));
|
||||||
Ok(ComplexSelector::new(components, false))
|
Ok(ComplexSelector::new(components, false))
|
||||||
} else {
|
} else {
|
||||||
Err((format!("Can't append {} to {}.", complex, parent), span).into())
|
Err((
|
||||||
|
format!(
|
||||||
|
"Can't append {} to {}.",
|
||||||
|
complex,
|
||||||
|
serialize_selector_list(&parent.0, visitor.options, span)
|
||||||
|
),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<SassResult<Vec<ComplexSelector>>>()?,
|
.collect::<SassResult<Vec<ComplexSelector>>>()?,
|
||||||
@ -129,40 +149,46 @@ pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser) -> SassResult
|
|||||||
.into_value())
|
.into_value())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_extend(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_extend(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let selector = args
|
let selector =
|
||||||
.get_err(0, "selector")?
|
args.get_err(0, "selector")?
|
||||||
.to_selector(parser, "selector", false)?;
|
.to_selector(visitor, "selector", false, args.span())?;
|
||||||
let target = args
|
let target =
|
||||||
.get_err(1, "extendee")?
|
args.get_err(1, "extendee")?
|
||||||
.to_selector(parser, "extendee", false)?;
|
.to_selector(visitor, "extendee", false, args.span())?;
|
||||||
let source = args
|
let source =
|
||||||
.get_err(2, "extender")?
|
args.get_err(2, "extender")?
|
||||||
.to_selector(parser, "extender", false)?;
|
.to_selector(visitor, "extender", false, args.span())?;
|
||||||
|
|
||||||
Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
Ok(ExtensionStore::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_replace(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_replace(
|
||||||
|
mut args: ArgumentResult,
|
||||||
|
visitor: &mut Visitor,
|
||||||
|
) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let selector = args
|
let selector =
|
||||||
.get_err(0, "selector")?
|
args.get_err(0, "selector")?
|
||||||
.to_selector(parser, "selector", true)?;
|
.to_selector(visitor, "selector", true, args.span())?;
|
||||||
let target = args
|
let target =
|
||||||
.get_err(1, "original")?
|
args.get_err(1, "original")?
|
||||||
.to_selector(parser, "original", true)?;
|
.to_selector(visitor, "original", true, args.span())?;
|
||||||
let source = args
|
let source =
|
||||||
.get_err(2, "replacement")?
|
args.get_err(2, "replacement")?
|
||||||
.to_selector(parser, "replacement", true)?;
|
.to_selector(visitor, "replacement", true, args.span())?;
|
||||||
Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
Ok(ExtensionStore::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let selector1 = args
|
let selector1 =
|
||||||
.get_err(0, "selector1")?
|
args.get_err(0, "selector1")?
|
||||||
.to_selector(parser, "selector1", true)?;
|
.to_selector(visitor, "selector1", true, args.span())?;
|
||||||
|
|
||||||
if selector1.contains_parent_selector() {
|
if selector1.contains_parent_selector() {
|
||||||
return Err((
|
return Err((
|
||||||
@ -172,9 +198,9 @@ pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser) -> SassRes
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let selector2 = args
|
let selector2 =
|
||||||
.get_err(1, "selector2")?
|
args.get_err(1, "selector2")?
|
||||||
.to_selector(parser, "selector2", true)?;
|
.to_selector(visitor, "selector2", true, args.span())?;
|
||||||
|
|
||||||
if selector2.contains_parent_selector() {
|
if selector2.contains_parent_selector() {
|
||||||
return Err((
|
return Err((
|
||||||
|
@ -1,21 +1,6 @@
|
|||||||
use super::{Builtin, GlobalFunctionMap};
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use num_bigint::BigInt;
|
pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
use num_traits::{Signed, ToPrimitive, Zero};
|
|
||||||
|
|
||||||
#[cfg(feature = "random")]
|
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs,
|
|
||||||
common::QuoteKind,
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "string")? {
|
match args.get_err(0, "string")? {
|
||||||
Value::String(mut i, q) => {
|
Value::String(mut i, q) => {
|
||||||
@ -30,7 +15,7 @@ pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "string")? {
|
match args.get_err(0, "string")? {
|
||||||
Value::String(mut i, q) => {
|
Value::String(mut i, q) => {
|
||||||
@ -45,14 +30,14 @@ pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "string")? {
|
match args.get_err(0, "string")? {
|
||||||
Value::String(i, _) => Ok(Value::Dimension(
|
Value::String(i, _) => Ok(Value::Dimension(SassNumber {
|
||||||
Some(Number::from(i.chars().count())),
|
num: (Number::from(i.chars().count())),
|
||||||
Unit::None,
|
unit: Unit::None,
|
||||||
true,
|
as_slash: None,
|
||||||
)),
|
})),
|
||||||
v => Err((
|
v => Err((
|
||||||
format!("$string: {} is not a string.", v.inspect(args.span())?),
|
format!("$string: {} is not a string.", v.inspect(args.span())?),
|
||||||
args.span(),
|
args.span(),
|
||||||
@ -61,7 +46,7 @@ pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "string")? {
|
match args.get_err(0, "string")? {
|
||||||
Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
|
Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
|
||||||
@ -73,7 +58,7 @@ pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
match args.get_err(0, "string")? {
|
match args.get_err(0, "string")? {
|
||||||
i @ Value::String(..) => Ok(i.unquote()),
|
i @ Value::String(..) => Ok(i.unquote()),
|
||||||
@ -85,8 +70,11 @@ pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
|
|
||||||
|
let span = args.span();
|
||||||
|
|
||||||
let (string, quotes) = match args.get_err(0, "string")? {
|
let (string, quotes) = match args.get_err(0, "string")? {
|
||||||
Value::String(s, q) => (s, q),
|
Value::String(s, q) => (s, q),
|
||||||
v => {
|
v => {
|
||||||
@ -97,79 +85,46 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let str_len = string.chars().count();
|
let str_len = string.chars().count();
|
||||||
let start = match args.get_err(1, "start-at")? {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
let start = args
|
||||||
return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
|
.get_err(1, "start-at")?
|
||||||
}
|
.assert_number_with_name("start-at", span)?;
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
|
start.assert_no_units("start-at", span)?;
|
||||||
n.to_integer().to_usize().unwrap_or(str_len + 1)
|
|
||||||
}
|
let start = start.num().assert_int(span)?;
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize,
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize,
|
let start = if start == 0 {
|
||||||
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
|
1
|
||||||
.to_usize()
|
} else if start > 0 {
|
||||||
.unwrap(),
|
(start as usize).min(str_len + 1)
|
||||||
Value::Dimension(None, Unit::None, ..) => {
|
} else {
|
||||||
return Err(("NaN is not an int.", args.span()).into())
|
(start + str_len as i32 + 1).max(1) as usize
|
||||||
}
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$start: Expected {} to have no units.",
|
|
||||||
v.inspect(args.span())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$start-at: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut end = match args.default_arg(2, "end-at", Value::Null)? {
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
|
||||||
return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
|
|
||||||
}
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
|
|
||||||
n.to_integer().to_usize().unwrap_or(str_len + 1)
|
|
||||||
}
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize,
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize,
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
|
|
||||||
.to_usize()
|
|
||||||
.unwrap_or(str_len + 1),
|
|
||||||
Value::Dimension(None, Unit::None, ..) => {
|
|
||||||
return Err(("NaN is not an int.", args.span()).into())
|
|
||||||
}
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$end: Expected {} to have no units.",
|
|
||||||
v.inspect(args.span())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
Value::Null => str_len,
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$end-at: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if end > str_len {
|
let end = args
|
||||||
end = str_len;
|
.default_arg(
|
||||||
|
2,
|
||||||
|
"end-at",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number(-1.0),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.assert_number_with_name("end-at", span)?;
|
||||||
|
|
||||||
|
end.assert_no_units("end-at", span)?;
|
||||||
|
|
||||||
|
let mut end = end.num().assert_int(span)?;
|
||||||
|
|
||||||
|
if end < 0 {
|
||||||
|
end += str_len as i32 + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let end = (end.max(0) as usize).min(str_len + 1);
|
||||||
|
|
||||||
if start > end || start > str_len {
|
if start > end || start > str_len {
|
||||||
Ok(Value::String(String::new(), quotes))
|
Ok(Value::String(String::new(), quotes))
|
||||||
} else {
|
} else {
|
||||||
@ -184,7 +139,7 @@ pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let s1 = match args.get_err(0, "string")? {
|
let s1 = match args.get_err(0, "string")? {
|
||||||
Value::String(i, _) => i,
|
Value::String(i, _) => i,
|
||||||
@ -209,19 +164,25 @@ pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<V
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(match s1.find(&substr) {
|
Ok(match s1.find(&substr) {
|
||||||
Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true),
|
Some(v) => Value::Dimension(SassNumber {
|
||||||
|
num: (Number::from(v + 1)),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
None => Value::Null,
|
None => Value::Null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
|
let span = args.span();
|
||||||
|
|
||||||
let (s1, quotes) = match args.get_err(0, "string")? {
|
let (s1, quotes) = match args.get_err(0, "string")? {
|
||||||
Value::String(i, q) => (i, q),
|
Value::String(i, q) => (i, q),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$string: {} is not a string.", v.inspect(args.span())?),
|
format!("$string: {} is not a string.", v.inspect(span)?),
|
||||||
args.span(),
|
span,
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
@ -231,43 +192,18 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
Value::String(i, _) => i,
|
Value::String(i, _) => i,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$insert: {} is not a string.", v.inspect(args.span())?),
|
format!("$insert: {} is not a string.", v.inspect(span)?),
|
||||||
args.span(),
|
span,
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let index = match args.get_err(2, "index")? {
|
let index = args
|
||||||
Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
|
.get_err(2, "index")?
|
||||||
return Err((
|
.assert_number_with_name("index", span)?;
|
||||||
format!("$index: {} is not an int.", n.inspect()),
|
index.assert_no_units("index", span)?;
|
||||||
args.span(),
|
let index_int = index.num().assert_int_with_name("index", span)?;
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
Value::Dimension(Some(n), Unit::None, _) => n,
|
|
||||||
Value::Dimension(None, Unit::None, ..) => {
|
|
||||||
return Err(("$index: NaN is not an int.", args.span()).into())
|
|
||||||
}
|
|
||||||
v @ Value::Dimension(..) => {
|
|
||||||
return Err((
|
|
||||||
format!(
|
|
||||||
"$index: Expected {} to have no units.",
|
|
||||||
v.inspect(args.span())?
|
|
||||||
),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err((
|
|
||||||
format!("$index: {} is not a number.", v.inspect(args.span())?),
|
|
||||||
args.span(),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if s1.is_empty() {
|
if s1.is_empty() {
|
||||||
return Ok(Value::String(substr, quotes));
|
return Ok(Value::String(substr, quotes));
|
||||||
@ -291,26 +227,13 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
};
|
};
|
||||||
|
|
||||||
let string = if index.is_positive() {
|
let string = if index_int > 0 {
|
||||||
insert(
|
insert((index_int as usize - 1).min(len), s1, &substr)
|
||||||
index
|
} else if index_int == 0 {
|
||||||
.to_integer()
|
|
||||||
.to_usize()
|
|
||||||
.unwrap_or(len + 1)
|
|
||||||
.min(len + 1)
|
|
||||||
- 1,
|
|
||||||
s1,
|
|
||||||
&substr,
|
|
||||||
)
|
|
||||||
} else if index.is_zero() {
|
|
||||||
insert(0, s1, &substr)
|
insert(0, s1, &substr)
|
||||||
} else {
|
} else {
|
||||||
let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1);
|
let idx = (len as i32 + index_int + 1).max(0) as usize;
|
||||||
if idx > len {
|
insert(idx, s1, &substr)
|
||||||
insert(0, s1, &substr)
|
|
||||||
} else {
|
|
||||||
insert(len - idx + 1, s1, &substr)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::String(string, quotes))
|
Ok(Value::String(string, quotes))
|
||||||
@ -318,15 +241,15 @@ pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<
|
|||||||
|
|
||||||
#[cfg(feature = "random")]
|
#[cfg(feature = "random")]
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub(crate) fn unique_id(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(0)?;
|
args.max_args(0)?;
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
let string = std::iter::repeat(())
|
let string: String = std::iter::repeat(())
|
||||||
.map(|()| rng.sample(Alphanumeric))
|
.map(|()| rng.sample(Alphanumeric))
|
||||||
.map(char::from)
|
.map(char::from)
|
||||||
.take(7)
|
.take(12)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Value::String(string, QuoteKind::None))
|
Ok(Value::String(format!("id-{}", string), QuoteKind::None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
|
||||||
|
@ -2,5 +2,32 @@ mod functions;
|
|||||||
pub(crate) mod modules;
|
pub(crate) mod modules;
|
||||||
|
|
||||||
pub(crate) use functions::{
|
pub(crate) use functions::{
|
||||||
color, list, map, math, meta, selector, string, Builtin, GLOBAL_FUNCTIONS,
|
color, list, map, math, meta, selector, string, Builtin, DISALLOWED_PLAIN_CSS_FUNCTION_NAMES,
|
||||||
|
GLOBAL_FUNCTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Imports common to all builtin fns
|
||||||
|
mod builtin_imports {
|
||||||
|
pub(crate) use super::functions::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS};
|
||||||
|
|
||||||
|
pub(crate) use codemap::{Span, Spanned};
|
||||||
|
|
||||||
|
#[cfg(feature = "random")]
|
||||||
|
pub(crate) use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
|
||||||
|
pub(crate) use crate::{
|
||||||
|
ast::{Argument, ArgumentDeclaration, ArgumentResult, MaybeEvaledArguments},
|
||||||
|
color::Color,
|
||||||
|
common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind},
|
||||||
|
error::SassResult,
|
||||||
|
evaluate::Visitor,
|
||||||
|
unit::Unit,
|
||||||
|
value::{CalculationArg, Number, SassFunction, SassMap, SassNumber, Value},
|
||||||
|
Options,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::{BTreeMap, BTreeSet},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,8 +1,32 @@
|
|||||||
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use crate::builtin::{
|
use crate::builtin::{
|
||||||
list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip},
|
list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip},
|
||||||
modules::Module,
|
modules::Module,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// todo: write tests for this
|
||||||
|
fn slash(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
args.min_args(1)?;
|
||||||
|
|
||||||
|
let span = args.span();
|
||||||
|
|
||||||
|
let list = if args.len() == 1 {
|
||||||
|
args.get_err(0, "elements")?.as_list()
|
||||||
|
} else {
|
||||||
|
args.get_variadic()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|arg| arg.node)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
if list.len() < 2 {
|
||||||
|
return Err(("At least two elements are required.", span).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::List(list, ListSeparator::Slash, Brackets::None))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut Module) {
|
pub(crate) fn declare(f: &mut Module) {
|
||||||
f.insert_builtin("append", append);
|
f.insert_builtin("append", append);
|
||||||
f.insert_builtin("index", index);
|
f.insert_builtin("index", index);
|
||||||
@ -13,4 +37,5 @@ pub(crate) fn declare(f: &mut Module) {
|
|||||||
f.insert_builtin("nth", nth);
|
f.insert_builtin("nth", nth);
|
||||||
f.insert_builtin("set-nth", set_nth);
|
f.insert_builtin("set-nth", set_nth);
|
||||||
f.insert_builtin("zip", zip);
|
f.insert_builtin("zip", zip);
|
||||||
|
f.insert_builtin("slash", slash);
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
use std::cmp::Ordering;
|
use crate::builtin::builtin_imports::*;
|
||||||
|
|
||||||
use num_traits::{One, Signed, Zero};
|
use crate::builtin::{
|
||||||
|
|
||||||
use crate::{
|
|
||||||
args::CallArgs,
|
|
||||||
builtin::{
|
|
||||||
math::{abs, ceil, comparable, divide, floor, max, min, percentage, round},
|
math::{abs, ceil, comparable, divide, floor, max, min, percentage, round},
|
||||||
meta::{unit, unitless},
|
meta::{unit, unitless},
|
||||||
modules::Module,
|
modules::Module,
|
||||||
},
|
|
||||||
common::Op,
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
unit::Unit,
|
|
||||||
value::{Number, Value},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "random")]
|
#[cfg(feature = "random")]
|
||||||
use crate::builtin::math::random;
|
use crate::builtin::math::random;
|
||||||
|
use crate::value::{conversion_factor, SassNumber};
|
||||||
|
|
||||||
fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn coerce_to_rad(num: f64, unit: Unit) -> f64 {
|
||||||
|
debug_assert!(matches!(
|
||||||
|
unit,
|
||||||
|
Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn
|
||||||
|
));
|
||||||
|
|
||||||
|
if unit == Unit::None {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
let factor = conversion_factor(&unit, &Unit::Rad).unwrap();
|
||||||
|
|
||||||
|
num * factor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(3)?;
|
args.max_args(3)?;
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
|
|
||||||
let min = match args.get_err(0, "min")? {
|
let min = match args.get_err(0, "min")? {
|
||||||
v @ Value::Dimension(..) => v,
|
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$min: {} is not a number.", v.inspect(args.span())?),
|
format!("$min: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -35,7 +41,7 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let number = match args.get_err(1, "number")? {
|
let number = match args.get_err(1, "number")? {
|
||||||
v @ Value::Dimension(..) => v,
|
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(span)?),
|
format!("$number: {} is not a number.", v.inspect(span)?),
|
||||||
@ -46,23 +52,35 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let max = match args.get_err(2, "max")? {
|
let max = match args.get_err(2, "max")? {
|
||||||
v @ Value::Dimension(..) => v,
|
v @ Value::Dimension(SassNumber { .. }) => v,
|
||||||
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
|
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ensure that `min` and `max` are compatible
|
// ensure that `min` and `max` are compatible
|
||||||
min.cmp(&max, span, Op::LessThan)?;
|
min.cmp(&max, span, BinaryOp::LessThan)?;
|
||||||
|
|
||||||
let min_unit = match min {
|
let min_unit = match min {
|
||||||
Value::Dimension(_, ref u, _) => u,
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: ref u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let number_unit = match number {
|
let number_unit = match number {
|
||||||
Value::Dimension(_, ref u, _) => u,
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: ref u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let max_unit = match max {
|
let max_unit = match max {
|
||||||
Value::Dimension(_, ref u, _) => u,
|
Value::Dimension(SassNumber {
|
||||||
|
num: _,
|
||||||
|
unit: ref u,
|
||||||
|
as_slash: _,
|
||||||
|
}) => u,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,45 +104,43 @@ fn clamp(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
), span).into());
|
), span).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
match min.cmp(&number, span, Op::LessThan)? {
|
match min.cmp(&number, span, BinaryOp::LessThan)? {
|
||||||
Ordering::Greater => return Ok(min),
|
Some(Ordering::Greater) => return Ok(min),
|
||||||
Ordering::Equal => return Ok(number),
|
Some(Ordering::Equal) => return Ok(number),
|
||||||
Ordering::Less => {}
|
Some(Ordering::Less) | None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match max.cmp(&number, span, Op::GreaterThan)? {
|
match max.cmp(&number, span, BinaryOp::GreaterThan)? {
|
||||||
Ordering::Less => return Ok(max),
|
Some(Ordering::Less) => return Ok(max),
|
||||||
Ordering::Equal => return Ok(number),
|
Some(Ordering::Equal) => return Ok(number),
|
||||||
Ordering::Greater => {}
|
Some(Ordering::Greater) | None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(number)
|
Ok(number)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.min_args(1)?;
|
args.min_args(1)?;
|
||||||
|
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
|
|
||||||
let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> {
|
let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> {
|
||||||
match v.node {
|
match v.node {
|
||||||
Value::Dimension(n, u, ..) => Ok((n, u)),
|
Value::Dimension(SassNumber { num, unit, .. }) => Ok((num, unit)),
|
||||||
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let first: (Number, Unit) = match numbers.next().unwrap()? {
|
let (n, u) = numbers.next().unwrap()?;
|
||||||
(Some(n), u) => (n.clone() * n, u),
|
let first: (Number, Unit) = (n * n, u);
|
||||||
(None, u) => return Ok(Value::Dimension(None, u, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let rest = numbers
|
let rest = numbers
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, val)| -> SassResult<Option<Number>> {
|
.map(|(idx, val)| -> SassResult<Number> {
|
||||||
let (number, unit) = val?;
|
let (number, unit) = val?;
|
||||||
if first.1 == Unit::None {
|
if first.1 == Unit::None {
|
||||||
if unit == Unit::None {
|
if unit == Unit::None {
|
||||||
Ok(number.map(|n| n.clone() * n))
|
Ok(number * number)
|
||||||
} else {
|
} else {
|
||||||
Err((
|
Err((
|
||||||
format!(
|
format!(
|
||||||
@ -149,9 +165,8 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
} else if first.1.comparable(&unit) {
|
} else if first.1.comparable(&unit) {
|
||||||
Ok(number
|
let n = number.convert(&unit, &first.1);
|
||||||
.map(|n| n.convert(&unit, &first.1))
|
Ok(n * n)
|
||||||
.map(|n| n.clone() * n))
|
|
||||||
} else {
|
} else {
|
||||||
Err((
|
Err((
|
||||||
format!("Incompatible units {} and {}.", first.1, unit),
|
format!("Incompatible units {} and {}.", first.1, unit),
|
||||||
@ -160,24 +175,28 @@ fn hypot(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<SassResult<Option<Vec<Number>>>>()?;
|
.collect::<SassResult<Vec<Number>>>()?;
|
||||||
|
|
||||||
let rest = match rest {
|
|
||||||
Some(v) => v,
|
|
||||||
None => return Ok(Value::Dimension(None, first.1, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b);
|
let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b);
|
||||||
|
|
||||||
Ok(Value::Dimension(sum.sqrt(), first.1, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: sum.sqrt(),
|
||||||
|
unit: first.1,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let number = match args.get_err(0, "number")? {
|
let number = match args.get_err(0, "number")? {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
// Value::Dimension { num: n, .. } if n.is_nan() => todo!(),
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
Value::Dimension(SassNumber {
|
||||||
|
num,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => num,
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be unitless.",
|
"$number: Expected {} to be unitless.",
|
||||||
@ -187,7 +206,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
v @ Value::Dimension(None, ..) => return Ok(v),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -197,10 +215,14 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let base = match args.default_arg(1, "base", Value::Null)? {
|
let base = match args.default_arg(1, "base", Value::Null) {
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => Some(n),
|
Value::Dimension(SassNumber {
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
num,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => Some(num),
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be unitless.",
|
"$number: Expected {} to be unitless.",
|
||||||
@ -210,7 +232,6 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
v @ Value::Dimension(None, ..) => return Ok(v),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -220,31 +241,36 @@ fn log(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::Dimension(
|
Ok(Value::Dimension(SassNumber {
|
||||||
if let Some(base) = base {
|
num: if let Some(base) = base {
|
||||||
if base.is_zero() {
|
if base.is_zero() {
|
||||||
Some(Number::zero())
|
Number::zero()
|
||||||
} else {
|
} else {
|
||||||
number.log(base)
|
number.log(base)
|
||||||
}
|
}
|
||||||
} else if number.is_negative() {
|
// todo: test with negative 0
|
||||||
None
|
} else if number.is_negative() && !number.is_zero() {
|
||||||
|
Number(f64::NAN)
|
||||||
} else if number.is_zero() {
|
} else if number.is_zero() {
|
||||||
todo!()
|
Number(f64::NEG_INFINITY)
|
||||||
} else {
|
} else {
|
||||||
number.ln()
|
number.ln()
|
||||||
},
|
},
|
||||||
Unit::None,
|
unit: Unit::None,
|
||||||
true,
|
as_slash: None,
|
||||||
))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let base = match args.get_err(0, "base")? {
|
let base = match args.get_err(0, "base")? {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
Value::Dimension(SassNumber {
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
num,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => num,
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$base: Expected {} to have no units.",
|
"$base: Expected {} to have no units.",
|
||||||
@ -254,7 +280,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
format!("$base: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -265,8 +290,12 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let exponent = match args.get_err(1, "exponent")? {
|
let exponent = match args.get_err(1, "exponent")? {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => n,
|
Value::Dimension(SassNumber {
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
num,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => num,
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$exponent: Expected {} to have no units.",
|
"$exponent: Expected {} to have no units.",
|
||||||
@ -276,7 +305,6 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$exponent: {} is not a number.", v.inspect(args.span())?),
|
format!("$exponent: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -286,16 +314,28 @@ fn pow(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::Dimension(base.pow(exponent), Unit::None, true))
|
Ok(Value::Dimension(SassNumber {
|
||||||
|
num: base.pow(exponent),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let number = args.get_err(0, "number")?;
|
let number = args.get_err(0, "number")?;
|
||||||
|
|
||||||
Ok(match number {
|
Ok(match number {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(n.sqrt(), Unit::None, true),
|
Value::Dimension(SassNumber {
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
num,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => Value::Dimension(SassNumber {
|
||||||
|
num: num.sqrt(),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to have no units.",
|
"$number: Expected {} to have no units.",
|
||||||
@ -305,7 +345,6 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -317,30 +356,32 @@ fn sqrt(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! trig_fn {
|
macro_rules! trig_fn {
|
||||||
($name:ident, $name_deg:ident) => {
|
($name:ident) => {
|
||||||
fn $name(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let number = args.get_err(0, "number")?;
|
let number = args.get_err(0, "number")?;
|
||||||
|
|
||||||
Ok(match number {
|
Ok(match number {
|
||||||
Value::Dimension(Some(n), Unit::None, ..)
|
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
|
||||||
| Value::Dimension(Some(n), Unit::Rad, ..) => {
|
Value::Dimension(SassNumber {
|
||||||
Value::Dimension(n.$name(), Unit::None, true)
|
num,
|
||||||
}
|
unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn),
|
||||||
Value::Dimension(Some(n), Unit::Deg, ..) => {
|
..
|
||||||
Value::Dimension(n.$name_deg(), Unit::None, true)
|
}) => Value::Dimension(SassNumber {
|
||||||
}
|
num: Number(coerce_to_rad(num.0, unit).$name()),
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
v @ Value::Dimension(..) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be an angle.",
|
"$number: Expected {} to have an angle unit (deg, grad, rad, turn).",
|
||||||
v.inspect(args.span())?
|
v.inspect(args.span())?
|
||||||
),
|
),
|
||||||
args.span(),
|
args.span(),
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -353,27 +394,31 @@ macro_rules! trig_fn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
trig_fn!(cos, cos_deg);
|
trig_fn!(cos);
|
||||||
trig_fn!(sin, sin_deg);
|
trig_fn!(sin);
|
||||||
trig_fn!(tan, tan_deg);
|
trig_fn!(tan);
|
||||||
|
|
||||||
fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let number = args.get_err(0, "number")?;
|
let number = args.get_err(0, "number")?;
|
||||||
|
|
||||||
Ok(match number {
|
Ok(match number {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(
|
Value::Dimension(SassNumber {
|
||||||
if n > Number::from(1) || n < Number::from(-1) {
|
num,
|
||||||
None
|
unit: Unit::None,
|
||||||
} else if n.is_one() {
|
..
|
||||||
Some(Number::zero())
|
}) => Value::Dimension(SassNumber {
|
||||||
|
num: if num > Number::from(1) || num < Number::from(-1) {
|
||||||
|
Number(f64::NAN)
|
||||||
|
} else if num.is_one() {
|
||||||
|
Number::zero()
|
||||||
} else {
|
} else {
|
||||||
n.acos()
|
num.acos()
|
||||||
},
|
},
|
||||||
Unit::Deg,
|
unit: Unit::Deg,
|
||||||
true,
|
as_slash: None,
|
||||||
),
|
}),
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be unitless.",
|
"$number: Expected {} to be unitless.",
|
||||||
@ -383,7 +428,6 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -394,21 +438,37 @@ fn acos(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let number = args.get_err(0, "number")?;
|
let number = args.get_err(0, "number")?;
|
||||||
|
|
||||||
Ok(match number {
|
Ok(match number {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => {
|
Value::Dimension(SassNumber {
|
||||||
if n > Number::from(1) || n < Number::from(-1) {
|
num,
|
||||||
return Ok(Value::Dimension(None, Unit::Deg, true));
|
unit: Unit::None,
|
||||||
} else if n.is_zero() {
|
..
|
||||||
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true));
|
}) => {
|
||||||
|
if num > Number::from(1) || num < Number::from(-1) {
|
||||||
|
return Ok(Value::Dimension(SassNumber {
|
||||||
|
num: Number(f64::NAN),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
}));
|
||||||
|
} else if num.is_zero() {
|
||||||
|
return Ok(Value::Dimension(SassNumber {
|
||||||
|
num: Number::zero(),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::Dimension(n.asin(), Unit::Deg, true)
|
Value::Dimension(SassNumber {
|
||||||
|
num: num.asin(),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be unitless.",
|
"$number: Expected {} to be unitless.",
|
||||||
@ -418,7 +478,6 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -429,19 +488,31 @@ fn asin(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
let number = args.get_err(0, "number")?;
|
let number = args.get_err(0, "number")?;
|
||||||
|
|
||||||
Ok(match number {
|
Ok(match number {
|
||||||
Value::Dimension(Some(n), Unit::None, ..) => {
|
Value::Dimension(SassNumber {
|
||||||
|
num: n,
|
||||||
|
unit: Unit::None,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
if n.is_zero() {
|
if n.is_zero() {
|
||||||
return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true));
|
return Ok(Value::Dimension(SassNumber {
|
||||||
|
num: (Number::zero()),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::Dimension(n.atan(), Unit::Deg, true)
|
Value::Dimension(SassNumber {
|
||||||
|
num: n.atan(),
|
||||||
|
unit: Unit::Deg,
|
||||||
|
as_slash: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
v @ Value::Dimension(Some(..), ..) => {
|
v @ Value::Dimension(SassNumber { .. }) => {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
"$number: Expected {} to be unitless.",
|
"$number: Expected {} to be unitless.",
|
||||||
@ -451,7 +522,6 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true),
|
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
format!("$number: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -462,10 +532,12 @@ fn atan(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
let (y_num, y_unit) = match args.get_err(0, "y")? {
|
let (y_num, y_unit) = match args.get_err(0, "y")? {
|
||||||
Value::Dimension(n, u, ..) => (n, u),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n, unit: u, ..
|
||||||
|
}) => (n, u),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$y: {} is not a number.", v.inspect(args.span())?),
|
format!("$y: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -476,7 +548,9 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (x_num, x_unit) = match args.get_err(1, "x")? {
|
let (x_num, x_unit) = match args.get_err(1, "x")? {
|
||||||
Value::Dimension(n, u, ..) => (n, u),
|
Value::Dimension(SassNumber {
|
||||||
|
num: n, unit: u, ..
|
||||||
|
}) => (n, u),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$x: {} is not a number.", v.inspect(args.span())?),
|
format!("$x: {} is not a number.", v.inspect(args.span())?),
|
||||||
@ -487,17 +561,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None {
|
let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None {
|
||||||
let x = match x_num {
|
(x_num, y_num)
|
||||||
Some(n) => n,
|
|
||||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let y = match y_num {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
(x, y)
|
|
||||||
} else if y_unit == Unit::None {
|
} else if y_unit == Unit::None {
|
||||||
return Err((
|
return Err((
|
||||||
format!(
|
format!(
|
||||||
@ -519,17 +583,7 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
} else if x_unit.comparable(&y_unit) {
|
} else if x_unit.comparable(&y_unit) {
|
||||||
let x = match x_num {
|
(x_num, y_num.convert(&y_unit, &x_unit))
|
||||||
Some(n) => n,
|
|
||||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let y = match y_num {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return Ok(Value::Dimension(None, Unit::Deg, true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
(x, y.convert(&y_unit, &x_unit))
|
|
||||||
} else {
|
} else {
|
||||||
return Err((
|
return Err((
|
||||||
format!("Incompatible units {} and {}.", y_unit, x_unit),
|
format!("Incompatible units {} and {}.", y_unit, x_unit),
|
||||||
@ -538,51 +592,11 @@ fn atan2(mut args: CallArgs, _: &mut Parser) -> SassResult<Value> {
|
|||||||
.into());
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(Value::Dimension(SassNumber {
|
||||||
match (
|
num: Number(y_num.0.atan2(x_num.0).to_degrees()),
|
||||||
NumberState::from_number(&x_num),
|
unit: Unit::Deg,
|
||||||
NumberState::from_number(&y_num),
|
as_slash: None,
|
||||||
) {
|
}))
|
||||||
(NumberState::Zero, NumberState::FiniteNegative) => {
|
|
||||||
Value::Dimension(Some(Number::from(-90)), Unit::Deg, true)
|
|
||||||
}
|
|
||||||
(NumberState::Zero, NumberState::Zero) | (NumberState::Finite, NumberState::Zero) => {
|
|
||||||
Value::Dimension(Some(Number::zero()), Unit::Deg, true)
|
|
||||||
}
|
|
||||||
(NumberState::Zero, NumberState::Finite) => {
|
|
||||||
Value::Dimension(Some(Number::from(90)), Unit::Deg, true)
|
|
||||||
}
|
|
||||||
(NumberState::Finite, NumberState::Finite)
|
|
||||||
| (NumberState::FiniteNegative, NumberState::Finite)
|
|
||||||
| (NumberState::Finite, NumberState::FiniteNegative)
|
|
||||||
| (NumberState::FiniteNegative, NumberState::FiniteNegative) => Value::Dimension(
|
|
||||||
y_num
|
|
||||||
.atan2(x_num)
|
|
||||||
.map(|n| (n * Number::from(180)) / Number::pi()),
|
|
||||||
Unit::Deg,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
(NumberState::FiniteNegative, NumberState::Zero) => {
|
|
||||||
Value::Dimension(Some(Number::from(180)), Unit::Deg, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NumberState {
|
|
||||||
Zero,
|
|
||||||
Finite,
|
|
||||||
FiniteNegative,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NumberState {
|
|
||||||
fn from_number(num: &Number) -> Self {
|
|
||||||
match (num.is_zero(), num.is_positive()) {
|
|
||||||
(true, _) => NumberState::Zero,
|
|
||||||
(false, true) => NumberState::Finite,
|
|
||||||
(false, false) => NumberState::FiniteNegative,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut Module) {
|
pub(crate) fn declare(f: &mut Module) {
|
||||||
@ -614,10 +628,58 @@ pub(crate) fn declare(f: &mut Module) {
|
|||||||
|
|
||||||
f.insert_builtin_var(
|
f.insert_builtin_var(
|
||||||
"e",
|
"e",
|
||||||
Value::Dimension(Some(Number::from(std::f64::consts::E)), Unit::None, true),
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(std::f64::consts::E),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
f.insert_builtin_var(
|
f.insert_builtin_var(
|
||||||
"pi",
|
"pi",
|
||||||
Value::Dimension(Some(Number::from(std::f64::consts::PI)), Unit::None, true),
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(std::f64::consts::PI),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
f.insert_builtin_var(
|
||||||
|
"epsilon",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(std::f64::EPSILON),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
f.insert_builtin_var(
|
||||||
|
"max-safe-integer",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(9007199254740991.0),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
f.insert_builtin_var(
|
||||||
|
"min-safe-integer",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(-9007199254740991.0),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
f.insert_builtin_var(
|
||||||
|
"max-number",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(f64::MAX),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
f.insert_builtin_var(
|
||||||
|
"min-number",
|
||||||
|
Value::Dimension(SassNumber {
|
||||||
|
num: Number::from(f64::MIN_POSITIVE),
|
||||||
|
unit: Unit::None,
|
||||||
|
as_slash: None,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
use codemap::Spanned;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::ast::{Configuration, ConfiguredValue};
|
||||||
args::CallArgs,
|
use crate::builtin::builtin_imports::*;
|
||||||
builtin::{
|
|
||||||
|
use crate::builtin::{
|
||||||
meta::{
|
meta::{
|
||||||
call, content_exists, feature_exists, function_exists, get_function,
|
call, content_exists, feature_exists, function_exists, get_function,
|
||||||
global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists,
|
global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists,
|
||||||
},
|
},
|
||||||
modules::{Module, ModuleConfig},
|
modules::Module,
|
||||||
},
|
|
||||||
error::SassResult,
|
|
||||||
parse::{Parser, Stmt},
|
|
||||||
value::Value,
|
|
||||||
};
|
};
|
||||||
|
use crate::serializer::serialize_calculation_arg;
|
||||||
|
|
||||||
fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
fn load_css(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<()> {
|
||||||
args.max_args(2)?;
|
args.max_args(2)?;
|
||||||
|
|
||||||
let span = args.span();
|
let span = args.span();
|
||||||
@ -30,19 +30,21 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let with = match args.default_arg(1, "with", Value::Null)? {
|
let with = match args.default_arg(1, "with", Value::Null) {
|
||||||
Value::Map(map) => Some(map),
|
Value::Map(map) => Some(map),
|
||||||
Value::Null => None,
|
Value::Null => None,
|
||||||
v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()),
|
v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: tests for `with`
|
let mut configuration = Configuration::empty();
|
||||||
if let Some(with) = with {
|
|
||||||
let mut config = ModuleConfig::default();
|
|
||||||
|
|
||||||
|
if let Some(with) = with {
|
||||||
|
visitor.emit_warning("`grass` does not currently support the $with parameter of load-css. This file will be imported the same way it would using `@import`.", args.span());
|
||||||
|
|
||||||
|
let mut values = BTreeMap::new();
|
||||||
for (key, value) in with {
|
for (key, value) in with {
|
||||||
let key = match key {
|
let name = match key.node {
|
||||||
Value::String(s, ..) => s,
|
Value::String(s, ..) => Identifier::from(s),
|
||||||
v => {
|
v => {
|
||||||
return Err((
|
return Err((
|
||||||
format!("$with key: {} is not a string.", v.inspect(span)?),
|
format!("$with key: {} is not a string.", v.inspect(span)?),
|
||||||
@ -52,24 +54,45 @@ fn load_css(mut args: CallArgs, parser: &mut Parser) -> SassResult<Vec<Stmt>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
config.insert(
|
if values.contains_key(&name) {
|
||||||
Spanned {
|
// todo: write test for this
|
||||||
node: key.into(),
|
return Err((
|
||||||
span,
|
format!("The variable {name} was configured twice."),
|
||||||
},
|
key.span,
|
||||||
value.span(span),
|
)
|
||||||
)?;
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, stmts) = parser.load_module(&url, &mut config)?;
|
values.insert(name, ConfiguredValue::explicit(value, args.span()));
|
||||||
|
|
||||||
Ok(stmts)
|
|
||||||
} else {
|
|
||||||
parser.parse_single_import(&url, span)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
configuration = Configuration::explicit(values, args.span());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _configuration = Arc::new(RefCell::new(configuration));
|
||||||
|
|
||||||
|
let style_sheet = visitor.load_style_sheet(url.as_ref(), false, args.span())?;
|
||||||
|
|
||||||
|
visitor.visit_stylesheet(style_sheet)?;
|
||||||
|
|
||||||
|
// todo: support the $with argument to load-css
|
||||||
|
// visitor.load_module(
|
||||||
|
// url.as_ref(),
|
||||||
|
// Some(Arc::clone(&configuration)),
|
||||||
|
// true,
|
||||||
|
// args.span(),
|
||||||
|
// |visitor, module, stylesheet| {
|
||||||
|
// // (*module).borrow()
|
||||||
|
// Ok(())
|
||||||
|
// },
|
||||||
|
// )?;
|
||||||
|
|
||||||
|
// Visitor::assert_configuration_is_empty(&configuration, true)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
|
|
||||||
let module = match args.get_err(0, "module")? {
|
let module = match args.get_err(0, "module")? {
|
||||||
@ -84,11 +107,15 @@ fn module_functions(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::Map(
|
Ok(Value::Map(
|
||||||
parser.modules.get(module.into(), args.span())?.functions(),
|
(*(*visitor.env.modules)
|
||||||
|
.borrow()
|
||||||
|
.get(module.into(), args.span())?)
|
||||||
|
.borrow()
|
||||||
|
.functions(args.span()),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
|
fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
args.max_args(1)?;
|
args.max_args(1)?;
|
||||||
|
|
||||||
let module = match args.get_err(0, "module")? {
|
let module = match args.get_err(0, "module")? {
|
||||||
@ -103,10 +130,66 @@ fn module_variables(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::Map(
|
Ok(Value::Map(
|
||||||
parser.modules.get(module.into(), args.span())?.variables(),
|
(*(*visitor.env.modules)
|
||||||
|
.borrow()
|
||||||
|
.get(module.into(), args.span())?)
|
||||||
|
.borrow()
|
||||||
|
.variables(args.span()),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn calc_args(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
args.max_args(1)?;
|
||||||
|
|
||||||
|
let calc = match args.get_err(0, "calc")? {
|
||||||
|
Value::Calculation(calc) => calc,
|
||||||
|
v => {
|
||||||
|
return Err((
|
||||||
|
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
|
||||||
|
args.span(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let args = calc
|
||||||
|
.args
|
||||||
|
.into_iter()
|
||||||
|
.map(|arg| {
|
||||||
|
Ok(match arg {
|
||||||
|
CalculationArg::Number(num) => Value::Dimension(num),
|
||||||
|
CalculationArg::Calculation(calc) => Value::Calculation(calc),
|
||||||
|
CalculationArg::String(s) | CalculationArg::Interpolation(s) => {
|
||||||
|
Value::String(s, QuoteKind::None)
|
||||||
|
}
|
||||||
|
CalculationArg::Operation { .. } => Value::String(
|
||||||
|
serialize_calculation_arg(&arg, visitor.options, args.span())?,
|
||||||
|
QuoteKind::None,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<SassResult<Vec<_>>>()?;
|
||||||
|
|
||||||
|
Ok(Value::List(args, ListSeparator::Comma, Brackets::None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult<Value> {
|
||||||
|
args.max_args(1)?;
|
||||||
|
|
||||||
|
let calc = match args.get_err(0, "calc")? {
|
||||||
|
Value::Calculation(calc) => calc,
|
||||||
|
v => {
|
||||||
|
return Err((
|
||||||
|
format!("$calc: {} is not a calculation.", v.inspect(args.span())?),
|
||||||
|
args.span(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::String(calc.name.to_string(), QuoteKind::Quoted))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn declare(f: &mut Module) {
|
pub(crate) fn declare(f: &mut Module) {
|
||||||
f.insert_builtin("feature-exists", feature_exists);
|
f.insert_builtin("feature-exists", feature_exists);
|
||||||
f.insert_builtin("inspect", inspect);
|
f.insert_builtin("inspect", inspect);
|
||||||
@ -121,6 +204,8 @@ pub(crate) fn declare(f: &mut Module) {
|
|||||||
f.insert_builtin("module-functions", module_functions);
|
f.insert_builtin("module-functions", module_functions);
|
||||||
f.insert_builtin("get-function", get_function);
|
f.insert_builtin("get-function", get_function);
|
||||||
f.insert_builtin("call", call);
|
f.insert_builtin("call", call);
|
||||||
|
f.insert_builtin("calc-args", calc_args);
|
||||||
|
f.insert_builtin("calc-name", calc_name);
|
||||||
|
|
||||||
f.insert_builtin_mixin("load-css", load_css);
|
f.insert_builtin_mixin("load-css", load_css);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{BTreeMap, HashSet},
|
||||||
|
fmt,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use codemap::{Span, Spanned};
|
use codemap::{Span, Spanned};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
args::CallArgs,
|
ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin},
|
||||||
atrule::mixin::{BuiltinMixin, Mixin},
|
|
||||||
builtin::Builtin,
|
builtin::Builtin,
|
||||||
common::{Identifier, QuoteKind},
|
common::Identifier,
|
||||||
error::SassResult,
|
error::SassResult,
|
||||||
parse::Parser,
|
evaluate::{Environment, Visitor},
|
||||||
scope::Scope,
|
selector::ExtensionStore,
|
||||||
|
utils::{BaseMapView, MapView, MergedMapView, PublicMemberMapView},
|
||||||
value::{SassFunction, SassMap, Value},
|
value::{SassFunction, SassMap, Value},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::builtin_imports::QuoteKind;
|
||||||
|
|
||||||
mod color;
|
mod color;
|
||||||
mod list;
|
mod list;
|
||||||
mod map;
|
mod map;
|
||||||
@ -21,51 +28,89 @@ mod meta;
|
|||||||
mod selector;
|
mod selector;
|
||||||
mod string;
|
mod string;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Module {
|
pub(crate) struct ForwardedModule {
|
||||||
pub scope: Scope,
|
inner: Arc<RefCell<Module>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
/// A module can itself import other modules
|
forward_rule: AstForwardRule,
|
||||||
pub modules: Modules,
|
|
||||||
|
|
||||||
/// Whether or not this module is builtin
|
|
||||||
/// e.g. `"sass:math"`
|
|
||||||
is_builtin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
impl ForwardedModule {
|
||||||
pub(crate) struct Modules(BTreeMap<Identifier, Module>);
|
pub fn if_necessary(
|
||||||
|
module: Arc<RefCell<Module>>,
|
||||||
#[derive(Debug, Default)]
|
rule: AstForwardRule,
|
||||||
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>);
|
) -> Arc<RefCell<Module>> {
|
||||||
|
if rule.prefix.is_none()
|
||||||
impl ModuleConfig {
|
&& rule.shown_mixins_and_functions.is_none()
|
||||||
/// Removes and returns element with name
|
&& rule.shown_variables.is_none()
|
||||||
pub fn get(&mut self, name: Identifier) -> Option<Value> {
|
&& rule
|
||||||
self.0.remove(&name)
|
.hidden_mixins_and_functions
|
||||||
}
|
.as_ref()
|
||||||
|
.map_or(false, HashSet::is_empty)
|
||||||
/// If this structure is not empty at the end of
|
&& rule
|
||||||
/// an `@use`, we must throw an error
|
.hidden_variables
|
||||||
pub fn is_empty(&self) -> bool {
|
.as_ref()
|
||||||
self.0.is_empty()
|
.map_or(false, HashSet::is_empty)
|
||||||
}
|
{
|
||||||
|
module
|
||||||
pub fn insert(&mut self, name: Spanned<Identifier>, value: Spanned<Value>) -> SassResult<()> {
|
|
||||||
if self.0.insert(name.node, value.node).is_some() {
|
|
||||||
Err((
|
|
||||||
"The same variable may only be configured once.",
|
|
||||||
name.span.merge(value.span),
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Arc::new(RefCell::new(Module::Forwarded(ForwardedModule {
|
||||||
|
inner: module,
|
||||||
|
forward_rule: rule,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct ModuleScope {
|
||||||
|
pub variables: Arc<dyn MapView<Value = Value>>,
|
||||||
|
pub mixins: Arc<dyn MapView<Value = Mixin>>,
|
||||||
|
pub functions: Arc<dyn MapView<Value = SassFunction>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleScope {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
variables: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||||
|
mixins: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||||
|
functions: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub(crate) enum Module {
|
||||||
|
Environment {
|
||||||
|
scope: ModuleScope,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
upstream: Vec<Module>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
extension_store: ExtensionStore,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
env: Environment,
|
||||||
|
},
|
||||||
|
Builtin {
|
||||||
|
scope: ModuleScope,
|
||||||
|
},
|
||||||
|
Forwarded(ForwardedModule),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct Modules(BTreeMap<Identifier, Arc<RefCell<Module>>>);
|
||||||
|
|
||||||
impl Modules {
|
impl Modules {
|
||||||
pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> {
|
pub fn new() -> Self {
|
||||||
|
Self(BTreeMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(
|
||||||
|
&mut self,
|
||||||
|
name: Identifier,
|
||||||
|
module: Arc<RefCell<Module>>,
|
||||||
|
span: Span,
|
||||||
|
) -> SassResult<()> {
|
||||||
if self.0.contains_key(&name) {
|
if self.0.contains_key(&name) {
|
||||||
return Err((
|
return Err((
|
||||||
format!("There's already a module with namespace \"{}\".", name),
|
format!("There's already a module with namespace \"{}\".", name),
|
||||||
@ -79,9 +124,9 @@ impl Modules {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, name: Identifier, span: Span) -> SassResult<&Module> {
|
pub fn get(&self, name: Identifier, span: Span) -> SassResult<Arc<RefCell<Module>>> {
|
||||||
match self.0.get(&name) {
|
match self.0.get(&name) {
|
||||||
Some(v) => Ok(v),
|
Some(v) => Ok(Arc::clone(v)),
|
||||||
None => Err((
|
None => Err((
|
||||||
format!(
|
format!(
|
||||||
"There is no module with the namespace \"{}\".",
|
"There is no module with the namespace \"{}\".",
|
||||||
@ -93,7 +138,11 @@ impl Modules {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mut(&mut self, name: Identifier, span: Span) -> SassResult<&mut Module> {
|
pub fn get_mut(
|
||||||
|
&mut self,
|
||||||
|
name: Identifier,
|
||||||
|
span: Span,
|
||||||
|
) -> SassResult<&mut Arc<RefCell<Module>>> {
|
||||||
match self.0.get_mut(&name) {
|
match self.0.get_mut(&name) {
|
||||||
Some(v) => Ok(v),
|
Some(v) => Ok(v),
|
||||||
None => Err((
|
None => Err((
|
||||||
@ -106,153 +155,217 @@ impl Modules {
|
|||||||
.into()),
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&mut self, other: Self) {
|
|
||||||
self.0.extend(other.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn member_map<V: fmt::Debug + Clone + 'static>(
|
||||||
|
local: Arc<dyn MapView<Value = V>>,
|
||||||
|
others: Vec<Arc<dyn MapView<Value = V>>>,
|
||||||
|
) -> Arc<dyn MapView<Value = V>> {
|
||||||
|
let local_map = PublicMemberMapView(local);
|
||||||
|
|
||||||
|
if others.is_empty() {
|
||||||
|
return Arc::new(local_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut all_maps: Vec<Arc<dyn MapView<Value = V>>> =
|
||||||
|
others.into_iter().filter(|map| !map.is_empty()).collect();
|
||||||
|
|
||||||
|
all_maps.push(Arc::new(local_map));
|
||||||
|
|
||||||
|
// todo: potential optimization when all_maps.len() == 1
|
||||||
|
Arc::new(MergedMapView::new(all_maps))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module {
|
impl Module {
|
||||||
|
pub fn new_env(env: Environment, extension_store: ExtensionStore) -> Self {
|
||||||
|
let variables = {
|
||||||
|
let variables = (*env.forwarded_modules).borrow();
|
||||||
|
let variables = variables
|
||||||
|
.iter()
|
||||||
|
.map(|module| Arc::clone(&(*module).borrow().scope().variables));
|
||||||
|
let this = Arc::new(BaseMapView(env.global_vars()));
|
||||||
|
member_map(this, variables.collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mixins = {
|
||||||
|
let mixins = (*env.forwarded_modules).borrow();
|
||||||
|
let mixins = mixins
|
||||||
|
.iter()
|
||||||
|
.map(|module| Arc::clone(&(*module).borrow().scope().mixins));
|
||||||
|
let this = Arc::new(BaseMapView(env.global_mixins()));
|
||||||
|
member_map(this, mixins.collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
let functions = {
|
||||||
|
let functions = (*env.forwarded_modules).borrow();
|
||||||
|
let functions = functions
|
||||||
|
.iter()
|
||||||
|
.map(|module| Arc::clone(&(*module).borrow().scope().functions));
|
||||||
|
let this = Arc::new(BaseMapView(env.global_functions()));
|
||||||
|
member_map(this, functions.collect())
|
||||||
|
};
|
||||||
|
|
||||||
|
let scope = ModuleScope {
|
||||||
|
variables,
|
||||||
|
mixins,
|
||||||
|
functions,
|
||||||
|
};
|
||||||
|
|
||||||
|
Module::Environment {
|
||||||
|
scope,
|
||||||
|
upstream: Vec::new(),
|
||||||
|
extension_store,
|
||||||
|
env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_builtin() -> Self {
|
pub fn new_builtin() -> Self {
|
||||||
Module {
|
Module::Builtin {
|
||||||
scope: Scope::default(),
|
scope: ModuleScope::new(),
|
||||||
modules: Modules::default(),
|
|
||||||
is_builtin: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
|
fn scope(&self) -> ModuleScope {
|
||||||
if name.node.as_str().starts_with('-') {
|
match self {
|
||||||
return Err((
|
Self::Builtin { scope } | Self::Environment { scope, .. } => scope.clone(),
|
||||||
"Private members can't be accessed from outside their modules.",
|
Self::Forwarded(forwarded) => (*forwarded.inner).borrow().scope(),
|
||||||
name.span,
|
}
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.scope.vars.get(&name.node) {
|
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<Value> {
|
||||||
|
let scope = self.scope();
|
||||||
|
|
||||||
|
match scope.variables.get(name.node) {
|
||||||
Some(v) => Ok(v),
|
Some(v) => Ok(v),
|
||||||
None => Err(("Undefined variable.", name.span).into()),
|
None => Err(("Undefined variable.", name.span).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_var_no_err(&self, name: Identifier) -> Option<Value> {
|
||||||
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.variables.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mixin_no_err(&self, name: Identifier) -> Option<Mixin> {
|
||||||
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.mixins.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_var(&mut self, name: Spanned<Identifier>, value: Value) -> SassResult<()> {
|
pub fn update_var(&mut self, name: Spanned<Identifier>, value: Value) -> SassResult<()> {
|
||||||
if self.is_builtin {
|
let scope = match self {
|
||||||
return Err(("Cannot modify built-in variable.", name.span).into());
|
Self::Builtin { .. } => {
|
||||||
|
return Err(("Cannot modify built-in variable.", name.span).into())
|
||||||
|
}
|
||||||
|
Self::Environment { scope, .. } => scope.clone(),
|
||||||
|
Self::Forwarded(forwarded) => (*forwarded.inner).borrow_mut().scope(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if scope.variables.insert(name.node, value).is_none() {
|
||||||
|
return Err(("Undefined variable.", name.span).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if name.node.as_str().starts_with('-') {
|
|
||||||
return Err((
|
|
||||||
"Private members can't be accessed from outside their modules.",
|
|
||||||
name.span,
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.scope.insert_var(name.node, value).is_some() {
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(("Undefined variable.", name.span).into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
|
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
|
||||||
if name.node.as_str().starts_with('-') {
|
let scope = self.scope();
|
||||||
return Err((
|
|
||||||
"Private members can't be accessed from outside their modules.",
|
|
||||||
name.span,
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.scope.mixins.get(&name.node) {
|
match scope.mixins.get(name.node) {
|
||||||
Some(v) => Ok(v.clone()),
|
Some(v) => Ok(v),
|
||||||
None => Err(("Undefined mixin.", name.span).into()),
|
None => Err(("Undefined mixin.", name.span).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) {
|
pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) {
|
||||||
self.scope.mixins.insert(name.into(), Mixin::Builtin(mixin));
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.mixins.insert(name.into(), Mixin::Builtin(mixin));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
|
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
|
||||||
self.scope.vars.insert(name.into(), value);
|
let ident = name.into();
|
||||||
|
|
||||||
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.variables.insert(ident, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fn(&self, name: Spanned<Identifier>) -> SassResult<Option<SassFunction>> {
|
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||||
if name.node.as_str().starts_with('-') {
|
let scope = self.scope();
|
||||||
return Err((
|
|
||||||
"Private members can't be accessed from outside their modules.",
|
|
||||||
name.span,
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.scope.functions.get(&name.node).cloned())
|
scope.functions.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn var_exists(&self, name: Identifier) -> bool {
|
pub fn var_exists(&self, name: Identifier) -> bool {
|
||||||
!name.as_str().starts_with('-') && self.scope.var_exists(name)
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.variables.get(name).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
pub fn mixin_exists(&self, name: Identifier) -> bool {
|
||||||
!name.as_str().starts_with('-') && self.scope.mixin_exists(name)
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.mixins.get(name).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fn_exists(&self, name: Identifier) -> bool {
|
pub fn fn_exists(&self, name: Identifier) -> bool {
|
||||||
!name.as_str().starts_with('-') && self.scope.fn_exists(name)
|
let scope = self.scope();
|
||||||
|
|
||||||
|
scope.functions.get(name).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_builtin(
|
pub fn insert_builtin(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
function: fn(CallArgs, &mut Parser) -> SassResult<Value>,
|
function: fn(ArgumentResult, &mut Visitor) -> SassResult<Value>,
|
||||||
) {
|
) {
|
||||||
let ident = name.into();
|
let ident = name.into();
|
||||||
self.scope
|
|
||||||
|
let scope = match self {
|
||||||
|
Self::Builtin { scope } => scope,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
scope
|
||||||
.functions
|
.functions
|
||||||
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn functions(&self) -> SassMap {
|
pub fn functions(&self, span: Span) -> SassMap {
|
||||||
SassMap::new_with(
|
SassMap::new_with(
|
||||||
self.scope
|
self.scope()
|
||||||
.functions
|
.functions
|
||||||
.iter()
|
.iter()
|
||||||
|
.into_iter()
|
||||||
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
(
|
(
|
||||||
Value::String(key.to_string(), QuoteKind::Quoted),
|
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
|
||||||
Value::FunctionRef(value.clone()),
|
Value::FunctionRef(value),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<(Value, Value)>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn variables(&self) -> SassMap {
|
pub fn variables(&self, span: Span) -> SassMap {
|
||||||
SassMap::new_with(
|
SassMap::new_with(
|
||||||
self.scope
|
self.scope()
|
||||||
.vars
|
.variables
|
||||||
.iter()
|
.iter()
|
||||||
|
.into_iter()
|
||||||
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
.filter(|(key, _)| !key.as_str().starts_with('-'))
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
(
|
(
|
||||||
Value::String(key.to_string(), QuoteKind::Quoted),
|
Value::String(key.to_string(), QuoteKind::Quoted).span(span),
|
||||||
value.clone(),
|
value,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<(Value, Value)>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn new_from_scope(scope: Scope, modules: Modules, is_builtin: bool) -> Self {
|
|
||||||
Module {
|
|
||||||
scope,
|
|
||||||
modules,
|
|
||||||
is_builtin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare_module_color() -> Module {
|
pub(crate) fn declare_module_color() -> Module {
|
||||||
|
397
src/color/mod.rs
397
src/color/mod.rs
@ -15,23 +15,28 @@
|
|||||||
//! Named colors retain their original casing,
|
//! Named colors retain their original casing,
|
||||||
//! so `rEd` should be emitted as `rEd`.
|
//! so `rEd` should be emitted as `rEd`.
|
||||||
|
|
||||||
use std::{
|
use crate::value::{fuzzy_round, Number};
|
||||||
cmp::{max, min},
|
|
||||||
fmt::{self, Display},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::value::Number;
|
|
||||||
pub(crate) use name::NAMED_COLORS;
|
pub(crate) use name::NAMED_COLORS;
|
||||||
|
|
||||||
use num_traits::{One, Signed, ToPrimitive, Zero};
|
|
||||||
|
|
||||||
mod name;
|
mod name;
|
||||||
|
|
||||||
|
// todo: only store alpha once on color
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Color {
|
pub(crate) struct Color {
|
||||||
rgba: Rgba,
|
rgba: Rgba,
|
||||||
hsla: Option<Hsla>,
|
hsla: Option<Hsla>,
|
||||||
repr: String,
|
pub format: ColorFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub(crate) enum ColorFormat {
|
||||||
|
Rgb,
|
||||||
|
Hsl,
|
||||||
|
/// Literal string from source text. Either a named color like `red` or a hex color
|
||||||
|
// todo: make this is a span and lookup text from codemap
|
||||||
|
Literal(String),
|
||||||
|
/// Use the most appropriate format
|
||||||
|
Infer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Color {
|
impl PartialEq for Color {
|
||||||
@ -48,12 +53,12 @@ impl Color {
|
|||||||
green: Number,
|
green: Number,
|
||||||
blue: Number,
|
blue: Number,
|
||||||
alpha: Number,
|
alpha: Number,
|
||||||
repr: String,
|
format: ColorFormat,
|
||||||
) -> Color {
|
) -> Color {
|
||||||
Color {
|
Color {
|
||||||
rgba: Rgba::new(red, green, blue, alpha),
|
rgba: Rgba::new(red, green, blue, alpha),
|
||||||
hsla: None,
|
hsla: None,
|
||||||
repr,
|
format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,12 +68,11 @@ impl Color {
|
|||||||
blue: Number,
|
blue: Number,
|
||||||
alpha: Number,
|
alpha: Number,
|
||||||
hsla: Hsla,
|
hsla: Hsla,
|
||||||
repr: String,
|
|
||||||
) -> Color {
|
) -> Color {
|
||||||
Color {
|
Color {
|
||||||
rgba: Rgba::new(red, green, blue, alpha),
|
rgba: Rgba::new(red, green, blue, alpha),
|
||||||
hsla: Some(hsla),
|
hsla: Some(hsla),
|
||||||
repr,
|
format: ColorFormat::Infer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,17 +88,17 @@ struct Rgba {
|
|||||||
impl PartialEq for Rgba {
|
impl PartialEq for Rgba {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
if self.red != other.red
|
if self.red != other.red
|
||||||
&& !(self.red >= Number::from(255) && other.red >= Number::from(255))
|
&& !(self.red >= Number::from(255.0) && other.red >= Number::from(255.0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if self.green != other.green
|
if self.green != other.green
|
||||||
&& !(self.green >= Number::from(255) && other.green >= Number::from(255))
|
&& !(self.green >= Number::from(255.0) && other.green >= Number::from(255.0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if self.blue != other.blue
|
if self.blue != other.blue
|
||||||
&& !(self.blue >= Number::from(255) && other.blue >= Number::from(255))
|
&& !(self.blue >= Number::from(255.0) && other.blue >= Number::from(255.0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -120,7 +124,7 @@ impl Rgba {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn alpha(&self) -> Number {
|
pub fn alpha(&self) -> Number {
|
||||||
self.alpha.clone()
|
self.alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,29 +147,29 @@ impl Hsla {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn hue(&self) -> Number {
|
pub fn hue(&self) -> Number {
|
||||||
self.hue.clone()
|
self.hue
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn saturation(&self) -> Number {
|
pub fn saturation(&self) -> Number {
|
||||||
self.saturation.clone()
|
self.saturation
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn luminance(&self) -> Number {
|
pub fn luminance(&self) -> Number {
|
||||||
self.luminance.clone()
|
self.luminance
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alpha(&self) -> Number {
|
pub fn alpha(&self) -> Number {
|
||||||
self.alpha.clone()
|
self.alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RGBA color functions
|
// RGBA color functions
|
||||||
impl Color {
|
impl Color {
|
||||||
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, repr: String) -> Self {
|
pub fn new(red: u8, green: u8, blue: u8, alpha: u8, format: String) -> Self {
|
||||||
Color {
|
Color {
|
||||||
rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()),
|
rgba: Rgba::new(red.into(), green.into(), blue.into(), alpha.into()),
|
||||||
hsla: None,
|
hsla: None,
|
||||||
repr,
|
format: ColorFormat::Literal(format),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,50 +181,62 @@ impl Color {
|
|||||||
mut blue: Number,
|
mut blue: Number,
|
||||||
mut alpha: Number,
|
mut alpha: Number,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
red = red.clamp(0, 255);
|
red = red.clamp(0.0, 255.0);
|
||||||
green = green.clamp(0, 255);
|
green = green.clamp(0.0, 255.0);
|
||||||
blue = blue.clamp(0, 255);
|
blue = blue.clamp(0.0, 255.0);
|
||||||
alpha = alpha.clamp(0, 1);
|
alpha = alpha.clamp(0.0, 1.0);
|
||||||
|
|
||||||
let repr = repr(&red, &green, &blue, &alpha);
|
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
|
||||||
Color::new_rgba(red, green, blue, alpha, repr)
|
}
|
||||||
|
|
||||||
|
pub fn from_rgba_fn(
|
||||||
|
mut red: Number,
|
||||||
|
mut green: Number,
|
||||||
|
mut blue: Number,
|
||||||
|
mut alpha: Number,
|
||||||
|
) -> Self {
|
||||||
|
red = red.clamp(0.0, 255.0);
|
||||||
|
green = green.clamp(0.0, 255.0);
|
||||||
|
blue = blue.clamp(0.0, 255.0);
|
||||||
|
alpha = alpha.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
Color::new_rgba(red, green, blue, alpha, ColorFormat::Rgb)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn red(&self) -> Number {
|
pub fn red(&self) -> Number {
|
||||||
self.rgba.red.clone().round()
|
self.rgba.red.round()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blue(&self) -> Number {
|
pub fn blue(&self) -> Number {
|
||||||
self.rgba.blue.clone().round()
|
self.rgba.blue.round()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn green(&self) -> Number {
|
pub fn green(&self) -> Number {
|
||||||
self.rgba.green.clone().round()
|
self.rgba.green.round()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mix two colors together with weight
|
/// Mix two colors together with weight
|
||||||
/// Algorithm adapted from
|
/// Algorithm adapted from
|
||||||
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
|
/// <https://github.com/sass/dart-sass/blob/0d0270cb12a9ac5cce73a4d0785fecb00735feee/lib/src/functions/color.dart#L718>
|
||||||
pub fn mix(self, other: &Color, weight: Number) -> Self {
|
pub fn mix(self, other: &Color, weight: Number) -> Self {
|
||||||
let weight = weight.clamp(0, 100);
|
let weight = weight.clamp(0.0, 100.0);
|
||||||
let normalized_weight = weight.clone() * Number::from(2) - Number::one();
|
let normalized_weight = weight * Number::from(2.0) - Number::one();
|
||||||
let alpha_distance = self.alpha() - other.alpha();
|
let alpha_distance = self.alpha() - other.alpha();
|
||||||
|
|
||||||
let combined_weight1 =
|
let combined_weight1 = if normalized_weight * alpha_distance == Number::from(-1) {
|
||||||
if normalized_weight.clone() * alpha_distance.clone() == Number::from(-1) {
|
|
||||||
normalized_weight
|
normalized_weight
|
||||||
} else {
|
} else {
|
||||||
(normalized_weight.clone() + alpha_distance.clone())
|
(normalized_weight + alpha_distance)
|
||||||
/ (Number::one() + normalized_weight * alpha_distance)
|
/ (Number::one() + normalized_weight * alpha_distance)
|
||||||
};
|
};
|
||||||
let weight1 = (combined_weight1 + Number::one()) / Number::from(2);
|
let weight1 = (combined_weight1 + Number::one()) / Number::from(2.0);
|
||||||
let weight2 = Number::one() - weight1.clone();
|
let weight2 = Number::one() - weight1;
|
||||||
|
|
||||||
Color::from_rgba(
|
Color::from_rgba(
|
||||||
self.red() * weight1.clone() + other.red() * weight2.clone(),
|
self.red() * weight1 + other.red() * weight2,
|
||||||
self.green() * weight1.clone() + other.green() * weight2.clone(),
|
self.green() * weight1 + other.green() * weight2,
|
||||||
self.blue() * weight1 + other.blue() * weight2,
|
self.blue() * weight1 + other.blue() * weight2,
|
||||||
self.alpha() * weight.clone() + other.alpha() * (Number::one() - weight),
|
self.alpha() * weight + other.alpha() * (Number::one() - weight),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,71 +250,71 @@ impl Color {
|
|||||||
return h.hue();
|
return h.hue();
|
||||||
}
|
}
|
||||||
|
|
||||||
let red = self.red() / Number::from(255);
|
let red = self.red() / Number::from(255.0);
|
||||||
let green = self.green() / Number::from(255);
|
let green = self.green() / Number::from(255.0);
|
||||||
let blue = self.blue() / Number::from(255);
|
let blue = self.blue() / Number::from(255.0);
|
||||||
|
|
||||||
let min = min(&red, min(&green, &blue)).clone();
|
let min = red.min(green.min(blue));
|
||||||
let max = max(&red, max(&green, &blue)).clone();
|
let max = red.max(green.max(blue));
|
||||||
|
|
||||||
let delta = max.clone() - min.clone();
|
let delta = max - min;
|
||||||
|
|
||||||
let hue = if min == max {
|
let hue = if min == max {
|
||||||
Number::zero()
|
Number::zero()
|
||||||
} else if max == red {
|
} else if max == red {
|
||||||
Number::from(60_u8) * (green - blue) / delta
|
Number::from(60.0) * (green - blue) / delta
|
||||||
} else if max == green {
|
} else if max == green {
|
||||||
Number::from(120_u8) + Number::from(60_u8) * (blue - red) / delta
|
Number::from(120.0) + Number::from(60.0) * (blue - red) / delta
|
||||||
} else {
|
} else {
|
||||||
Number::from(240_u8) + Number::from(60_u8) * (red - green) / delta
|
Number::from(240.0) + Number::from(60.0) * (red - green) / delta
|
||||||
};
|
};
|
||||||
|
|
||||||
hue % Number::from(360)
|
hue % Number::from(360.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate saturation from RGBA values
|
/// Calculate saturation from RGBA values
|
||||||
pub fn saturation(&self) -> Number {
|
pub fn saturation(&self) -> Number {
|
||||||
if let Some(h) = &self.hsla {
|
if let Some(h) = &self.hsla {
|
||||||
return h.saturation() * Number::from(100);
|
return h.saturation() * Number::from(100.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let red: Number = self.red() / Number::from(255);
|
let red: Number = self.red() / Number::from(255.0);
|
||||||
let green = self.green() / Number::from(255);
|
let green = self.green() / Number::from(255.0);
|
||||||
let blue = self.blue() / Number::from(255);
|
let blue = self.blue() / Number::from(255.0);
|
||||||
|
|
||||||
let min = min(&red, min(&green, &blue)).clone();
|
let min = red.min(green.min(blue));
|
||||||
let max = red.max(green.max(blue));
|
let max = red.max(green.max(blue));
|
||||||
|
|
||||||
if min == max {
|
if min == max {
|
||||||
return Number::zero();
|
return Number::zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
let delta = max.clone() - min.clone();
|
let delta = max - min;
|
||||||
|
|
||||||
let sum = max + min;
|
let sum = max + min;
|
||||||
|
|
||||||
let s = delta
|
let s = delta
|
||||||
/ if sum > Number::one() {
|
/ if sum > Number::one() {
|
||||||
Number::from(2) - sum
|
Number::from(2.0) - sum
|
||||||
} else {
|
} else {
|
||||||
sum
|
sum
|
||||||
};
|
};
|
||||||
|
|
||||||
s * Number::from(100)
|
s * Number::from(100.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate luminance from RGBA values
|
/// Calculate luminance from RGBA values
|
||||||
pub fn lightness(&self) -> Number {
|
pub fn lightness(&self) -> Number {
|
||||||
if let Some(h) = &self.hsla {
|
if let Some(h) = &self.hsla {
|
||||||
return h.luminance() * Number::from(100);
|
return h.luminance() * Number::from(100.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let red: Number = self.red() / Number::from(255);
|
let red: Number = self.red() / Number::from(255.0);
|
||||||
let green = self.green() / Number::from(255);
|
let green = self.green() / Number::from(255.0);
|
||||||
let blue = self.blue() / Number::from(255);
|
let blue = self.blue() / Number::from(255.0);
|
||||||
let min = min(&red, min(&green, &blue)).clone();
|
let min = red.min(green.min(blue));
|
||||||
let max = red.max(green.max(blue));
|
let max = red.max(green.max(blue));
|
||||||
(((min + max) / Number::from(2)) * Number::from(100)).round()
|
(((min + max) / Number::from(2.0)) * Number::from(100.0)).round()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_hsla(&self) -> (Number, Number, Number, Number) {
|
pub fn as_hsla(&self) -> (Number, Number, Number, Number) {
|
||||||
@ -306,21 +322,21 @@ impl Color {
|
|||||||
return (h.hue(), h.saturation(), h.luminance(), h.alpha());
|
return (h.hue(), h.saturation(), h.luminance(), h.alpha());
|
||||||
}
|
}
|
||||||
|
|
||||||
let red = self.red() / Number::from(255);
|
let red = self.red() / Number::from(255.0);
|
||||||
let green = self.green() / Number::from(255);
|
let green = self.green() / Number::from(255.0);
|
||||||
let blue = self.blue() / Number::from(255);
|
let blue = self.blue() / Number::from(255.0);
|
||||||
let min = min(&red, min(&green, &blue)).clone();
|
let min = red.min(green.min(blue));
|
||||||
let max = max(&red, max(&green, &blue)).clone();
|
let max = red.max(green.max(blue));
|
||||||
|
|
||||||
let lightness = (min.clone() + max.clone()) / Number::from(2);
|
let lightness = (min + max) / Number::from(2.0);
|
||||||
|
|
||||||
let saturation = if min == max {
|
let saturation = if min == max {
|
||||||
Number::zero()
|
Number::zero()
|
||||||
} else {
|
} else {
|
||||||
let d = max.clone() - min.clone();
|
let d = max - min;
|
||||||
let mm = max.clone() + min.clone();
|
let mm = max + min;
|
||||||
d / if mm > Number::one() {
|
d / if mm > Number::one() {
|
||||||
Number::from(2) - mm
|
Number::from(2.0) - mm
|
||||||
} else {
|
} else {
|
||||||
mm
|
mm
|
||||||
}
|
}
|
||||||
@ -329,20 +345,20 @@ impl Color {
|
|||||||
let mut hue = if min == max {
|
let mut hue = if min == max {
|
||||||
Number::zero()
|
Number::zero()
|
||||||
} else if blue == max {
|
} else if blue == max {
|
||||||
Number::from(4) + (red - green) / (max - min)
|
Number::from(4.0) + (red - green) / (max - min)
|
||||||
} else if green == max {
|
} else if green == max {
|
||||||
Number::from(2) + (blue - red) / (max - min)
|
Number::from(2.0) + (blue - red) / (max - min)
|
||||||
} else {
|
} else {
|
||||||
(green - blue) / (max - min)
|
(green - blue) / (max - min)
|
||||||
};
|
};
|
||||||
|
|
||||||
if hue.is_negative() {
|
if hue.is_negative() {
|
||||||
hue += Number::from(360);
|
hue += Number::from(360.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
hue *= Number::from(60);
|
hue *= Number::from(60.0);
|
||||||
|
|
||||||
(hue, saturation, lightness, self.alpha())
|
(hue % Number(360.0), saturation, lightness, self.alpha())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjust_hue(&self, degrees: Number) -> Self {
|
pub fn adjust_hue(&self, degrees: Number) -> Self {
|
||||||
@ -362,92 +378,67 @@ impl Color {
|
|||||||
|
|
||||||
pub fn saturate(&self, amount: Number) -> Self {
|
pub fn saturate(&self, amount: Number) -> Self {
|
||||||
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
||||||
Color::from_hsla(hue, saturation + amount, luminance, alpha)
|
Color::from_hsla(hue, (saturation + amount).clamp(0.0, 1.0), luminance, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn desaturate(&self, amount: Number) -> Self {
|
pub fn desaturate(&self, amount: Number) -> Self {
|
||||||
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
let (hue, saturation, luminance, alpha) = self.as_hsla();
|
||||||
Color::from_hsla(hue, saturation - amount, luminance, alpha)
|
Color::from_hsla(hue, (saturation - amount).clamp(0.0, 1.0), luminance, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hsla_fn(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self {
|
||||||
|
let mut color = Self::from_hsla(hue, saturation, luminance, alpha);
|
||||||
|
color.format = ColorFormat::Hsl;
|
||||||
|
color
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create RGBA representation from HSLA values
|
/// Create RGBA representation from HSLA values
|
||||||
pub fn from_hsla(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self {
|
pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self {
|
||||||
let mut hue = if hue >= Number::from(360) {
|
|
||||||
hue % Number::from(360)
|
|
||||||
} else if hue < Number::from(-360) {
|
|
||||||
Number::from(360) + hue % Number::from(360)
|
|
||||||
} else if hue.is_negative() {
|
|
||||||
Number::from(360) + hue.clamp(-360, 360)
|
|
||||||
} else {
|
|
||||||
hue
|
|
||||||
};
|
|
||||||
|
|
||||||
let saturation = saturation.clamp(0, 1);
|
|
||||||
let luminance = luminance.clamp(0, 1);
|
|
||||||
let alpha = alpha.clamp(0, 1);
|
|
||||||
|
|
||||||
let hsla = Hsla::new(
|
let hsla = Hsla::new(
|
||||||
hue.clone(),
|
hue,
|
||||||
saturation.clone(),
|
saturation.clamp(0.0, 1.0),
|
||||||
luminance.clone(),
|
lightness.clamp(0.0, 1.0),
|
||||||
alpha.clone(),
|
alpha,
|
||||||
);
|
);
|
||||||
|
|
||||||
if saturation.is_zero() {
|
let scaled_hue = hue.0 / 360.0;
|
||||||
let val = luminance * Number::from(255);
|
let scaled_saturation = saturation.0.clamp(0.0, 1.0);
|
||||||
let repr = repr(&val, &val, &val, &alpha);
|
let scaled_lightness = lightness.0.clamp(0.0, 1.0);
|
||||||
return Color::new_hsla(val.clone(), val.clone(), val, alpha, hsla, repr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let temporary_1 = if luminance < Number::small_ratio(1, 2) {
|
let m2 = if scaled_lightness <= 0.5 {
|
||||||
luminance.clone() * (Number::one() + saturation)
|
scaled_lightness * (scaled_saturation + 1.0)
|
||||||
} else {
|
} else {
|
||||||
luminance.clone() + saturation.clone() - luminance.clone() * saturation
|
scaled_lightness.mul_add(-scaled_saturation, scaled_lightness + scaled_saturation)
|
||||||
};
|
};
|
||||||
let temporary_2 = Number::from(2) * luminance - temporary_1.clone();
|
|
||||||
hue /= Number::from(360);
|
|
||||||
let mut temporary_r = hue.clone() + Number::small_ratio(1, 3);
|
|
||||||
let mut temporary_g = hue.clone();
|
|
||||||
let mut temporary_b = hue - Number::small_ratio(1, 3);
|
|
||||||
|
|
||||||
macro_rules! clamp_temp {
|
let m1 = scaled_lightness.mul_add(2.0, -m2);
|
||||||
($temp:ident) => {
|
|
||||||
if $temp > Number::one() {
|
let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0);
|
||||||
$temp -= Number::one();
|
let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0);
|
||||||
} else if $temp.is_negative() {
|
let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0);
|
||||||
$temp += Number::one();
|
|
||||||
}
|
Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla)
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clamp_temp!(temporary_r);
|
fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 {
|
||||||
clamp_temp!(temporary_g);
|
if hue < 0.0 {
|
||||||
clamp_temp!(temporary_b);
|
hue += 1.0;
|
||||||
|
}
|
||||||
|
if hue > 1.0 {
|
||||||
|
hue -= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
fn channel(temp: Number, temp1: &Number, temp2: &Number) -> Number {
|
if hue < 1.0 / 6.0 {
|
||||||
Number::from(255)
|
((m2 - m1) * hue).mul_add(6.0, m1)
|
||||||
* if Number::from(6) * temp.clone() < Number::one() {
|
} else if hue < 1.0 / 2.0 {
|
||||||
temp2.clone() + (temp1.clone() - temp2.clone()) * Number::from(6) * temp
|
m2
|
||||||
} else if Number::from(2) * temp.clone() < Number::one() {
|
} else if hue < 2.0 / 3.0 {
|
||||||
temp1.clone()
|
((m2 - m1) * (2.0 / 3.0 - hue)).mul_add(6.0, m1)
|
||||||
} else if Number::from(3) * temp.clone() < Number::from(2) {
|
|
||||||
temp2.clone()
|
|
||||||
+ (temp1.clone() - temp2.clone())
|
|
||||||
* (Number::small_ratio(2, 3) - temp)
|
|
||||||
* Number::from(6)
|
|
||||||
} else {
|
} else {
|
||||||
temp2.clone()
|
m1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let red = channel(temporary_r, &temporary_1, &temporary_2);
|
|
||||||
let green = channel(temporary_g, &temporary_1, &temporary_2);
|
|
||||||
let blue = channel(temporary_b, &temporary_1, &temporary_2);
|
|
||||||
|
|
||||||
let repr = repr(&red, &green, &blue, &alpha);
|
|
||||||
Color::new_hsla(red, green, blue, alpha, hsla, repr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn invert(&self, weight: Number) -> Self {
|
pub fn invert(&self, weight: Number) -> Self {
|
||||||
if weight.is_zero() {
|
if weight.is_zero() {
|
||||||
return self.clone();
|
return self.clone();
|
||||||
@ -456,9 +447,8 @@ impl Color {
|
|||||||
let red = Number::from(u8::max_value()) - self.red();
|
let red = Number::from(u8::max_value()) - self.red();
|
||||||
let green = Number::from(u8::max_value()) - self.green();
|
let green = Number::from(u8::max_value()) - self.green();
|
||||||
let blue = Number::from(u8::max_value()) - self.blue();
|
let blue = Number::from(u8::max_value()) - self.blue();
|
||||||
let repr = repr(&red, &green, &blue, &self.alpha());
|
|
||||||
|
|
||||||
let inverse = Color::new_rgba(red, green, blue, self.alpha(), repr);
|
let inverse = Color::new_rgba(red, green, blue, self.alpha(), ColorFormat::Infer);
|
||||||
|
|
||||||
inverse.mix(self, weight)
|
inverse.mix(self, weight)
|
||||||
}
|
}
|
||||||
@ -475,7 +465,7 @@ impl Color {
|
|||||||
pub fn alpha(&self) -> Number {
|
pub fn alpha(&self) -> Number {
|
||||||
let a = self.rgba.alpha();
|
let a = self.rgba.alpha();
|
||||||
if a > Number::one() {
|
if a > Number::one() {
|
||||||
a / Number::from(255)
|
a / Number::from(255.0)
|
||||||
} else {
|
} else {
|
||||||
a
|
a
|
||||||
}
|
}
|
||||||
@ -505,108 +495,41 @@ impl Color {
|
|||||||
impl Color {
|
impl Color {
|
||||||
pub fn to_ie_hex_str(&self) -> String {
|
pub fn to_ie_hex_str(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"#{:X}{:X}{:X}{:X}",
|
"#{:02X}{:02X}{:02X}{:02X}",
|
||||||
(self.alpha() * Number::from(255)).round().to_integer(),
|
fuzzy_round(self.alpha().0 * 255.0) as u8,
|
||||||
self.red().to_integer(),
|
self.red().0 as u8,
|
||||||
self.green().to_integer(),
|
self.green().0 as u8,
|
||||||
self.blue().to_integer()
|
self.blue().0 as u8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HWB color functions
|
/// HWB color functions
|
||||||
impl Color {
|
impl Color {
|
||||||
pub fn from_hwb(
|
pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color {
|
||||||
mut hue: Number,
|
let hue = Number(hue.rem_euclid(360.0) / 360.0);
|
||||||
mut white: Number,
|
let mut scaled_white = white.0 / 100.0;
|
||||||
mut black: Number,
|
let mut scaled_black = black.0 / 100.0;
|
||||||
mut alpha: Number,
|
alpha = alpha.clamp(0.0, 1.0);
|
||||||
) -> Color {
|
|
||||||
hue %= Number::from(360);
|
|
||||||
hue /= Number::from(360);
|
|
||||||
white /= Number::from(100);
|
|
||||||
black /= Number::from(100);
|
|
||||||
alpha = alpha.clamp(Number::zero(), Number::one());
|
|
||||||
|
|
||||||
let white_black_sum = white.clone() + black.clone();
|
let white_black_sum = scaled_white + scaled_black;
|
||||||
|
|
||||||
if white_black_sum > Number::one() {
|
if white_black_sum > 1.0 {
|
||||||
white /= white_black_sum.clone();
|
scaled_white /= white_black_sum;
|
||||||
black /= white_black_sum;
|
scaled_black /= white_black_sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
let factor = Number::one() - white.clone() - black;
|
let factor = 1.0 - scaled_white - scaled_black;
|
||||||
|
|
||||||
fn channel(m1: Number, m2: Number, mut hue: Number) -> Number {
|
let to_rgb = |hue: f64| -> Number {
|
||||||
if hue < Number::zero() {
|
let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white);
|
||||||
hue += Number::one();
|
Number(fuzzy_round(channel * 255.0))
|
||||||
}
|
|
||||||
|
|
||||||
if hue > Number::one() {
|
|
||||||
hue -= Number::one();
|
|
||||||
}
|
|
||||||
|
|
||||||
if hue < Number::small_ratio(1, 6) {
|
|
||||||
m1.clone() + (m2 - m1) * hue * Number::from(6)
|
|
||||||
} else if hue < Number::small_ratio(1, 2) {
|
|
||||||
m2
|
|
||||||
} else if hue < Number::small_ratio(2, 3) {
|
|
||||||
m1.clone() + (m2 - m1) * (Number::small_ratio(2, 3) - hue) * Number::from(6)
|
|
||||||
} else {
|
|
||||||
m1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let to_rgb = |hue: Number| -> Number {
|
|
||||||
let channel =
|
|
||||||
channel(Number::zero(), Number::one(), hue) * factor.clone() + white.clone();
|
|
||||||
channel * Number::from(255)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let red = to_rgb(hue.clone() + Number::small_ratio(1, 3));
|
let red = to_rgb(hue.0 + 1.0 / 3.0);
|
||||||
let green = to_rgb(hue.clone());
|
let green = to_rgb(hue.0);
|
||||||
let blue = to_rgb(hue - Number::small_ratio(1, 3));
|
let blue = to_rgb(hue.0 - 1.0 / 3.0);
|
||||||
|
|
||||||
let repr = repr(&red, &green, &blue, &alpha);
|
Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer)
|
||||||
|
|
||||||
Color::new_rgba(red, green, blue, alpha, repr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the proper representation from RGBA values
|
|
||||||
fn repr(red: &Number, green: &Number, blue: &Number, alpha: &Number) -> String {
|
|
||||||
fn into_u8(channel: &Number) -> u8 {
|
|
||||||
if channel > &Number::from(255) {
|
|
||||||
255_u8
|
|
||||||
} else if channel.is_negative() {
|
|
||||||
0_u8
|
|
||||||
} else {
|
|
||||||
channel.round().to_integer().to_u8().unwrap_or(255)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let red_u8 = into_u8(red);
|
|
||||||
let green_u8 = into_u8(green);
|
|
||||||
let blue_u8 = into_u8(blue);
|
|
||||||
|
|
||||||
if alpha < &Number::one() {
|
|
||||||
format!(
|
|
||||||
"rgba({}, {}, {}, {})",
|
|
||||||
red_u8,
|
|
||||||
green_u8,
|
|
||||||
blue_u8,
|
|
||||||
// todo: is_compressed
|
|
||||||
alpha.inspect()
|
|
||||||
)
|
|
||||||
} else if let Some(c) = NAMED_COLORS.get_by_rgba([red_u8, green_u8, blue_u8]) {
|
|
||||||
(*c).to_owned()
|
|
||||||
} else {
|
|
||||||
format!("#{:0>2x}{:0>2x}{:0>2x}", red_u8, green_u8, blue_u8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Color {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.repr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
117
src/common.rs
117
src/common.rs
@ -3,7 +3,16 @@ use std::fmt::{self, Display, Write};
|
|||||||
use crate::interner::InternedString;
|
use crate::interner::InternedString;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Op {
|
pub enum UnaryOp {
|
||||||
|
Plus,
|
||||||
|
Neg,
|
||||||
|
Div,
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BinaryOp {
|
||||||
|
SingleEq,
|
||||||
Equal,
|
Equal,
|
||||||
NotEqual,
|
NotEqual,
|
||||||
GreaterThan,
|
GreaterThan,
|
||||||
@ -14,51 +23,43 @@ pub enum Op {
|
|||||||
Minus,
|
Minus,
|
||||||
Mul,
|
Mul,
|
||||||
Div,
|
Div,
|
||||||
|
// todo: maybe rename mod, since it is mod
|
||||||
Rem,
|
Rem,
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
Not,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Op {
|
impl BinaryOp {
|
||||||
|
pub fn precedence(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::SingleEq => 0,
|
||||||
|
Self::Or => 1,
|
||||||
|
Self::And => 2,
|
||||||
|
Self::Equal | Self::NotEqual => 3,
|
||||||
|
Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan | Self::LessThanEqual => 4,
|
||||||
|
Self::Plus | Self::Minus => 5,
|
||||||
|
Self::Mul | Self::Div | Self::Rem => 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BinaryOp {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Equal => write!(f, "=="),
|
BinaryOp::SingleEq => write!(f, "="),
|
||||||
Self::NotEqual => write!(f, "!="),
|
BinaryOp::Equal => write!(f, "=="),
|
||||||
Self::GreaterThanEqual => write!(f, ">="),
|
BinaryOp::NotEqual => write!(f, "!="),
|
||||||
Self::LessThanEqual => write!(f, "<="),
|
BinaryOp::GreaterThanEqual => write!(f, ">="),
|
||||||
Self::GreaterThan => write!(f, ">"),
|
BinaryOp::LessThanEqual => write!(f, "<="),
|
||||||
Self::LessThan => write!(f, "<"),
|
BinaryOp::GreaterThan => write!(f, ">"),
|
||||||
Self::Plus => write!(f, "+"),
|
BinaryOp::LessThan => write!(f, "<"),
|
||||||
Self::Minus => write!(f, "-"),
|
BinaryOp::Plus => write!(f, "+"),
|
||||||
Self::Mul => write!(f, "*"),
|
BinaryOp::Minus => write!(f, "-"),
|
||||||
Self::Div => write!(f, "/"),
|
BinaryOp::Mul => write!(f, "*"),
|
||||||
Self::Rem => write!(f, "%"),
|
BinaryOp::Div => write!(f, "/"),
|
||||||
Self::And => write!(f, "and"),
|
BinaryOp::Rem => write!(f, "%"),
|
||||||
Self::Or => write!(f, "or"),
|
BinaryOp::And => write!(f, "and"),
|
||||||
Self::Not => write!(f, "not"),
|
BinaryOp::Or => write!(f, "or"),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Op {
|
|
||||||
/// Get order of precedence for an operator
|
|
||||||
///
|
|
||||||
/// Higher numbers are evaluated first.
|
|
||||||
/// Do not rely on the number itself, but rather the size relative to other numbers
|
|
||||||
///
|
|
||||||
/// If precedence is equal, the leftmost operation is evaluated first
|
|
||||||
pub fn precedence(self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::And | Self::Or | Self::Not => 0,
|
|
||||||
Self::Equal
|
|
||||||
| Self::NotEqual
|
|
||||||
| Self::GreaterThan
|
|
||||||
| Self::GreaterThanEqual
|
|
||||||
| Self::LessThan
|
|
||||||
| Self::LessThanEqual => 1,
|
|
||||||
Self::Plus | Self::Minus => 2,
|
|
||||||
Self::Mul | Self::Div | Self::Rem => 3,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,31 +86,47 @@ pub(crate) enum Brackets {
|
|||||||
Bracketed,
|
Bracketed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Eq)]
|
||||||
pub(crate) enum ListSeparator {
|
pub(crate) enum ListSeparator {
|
||||||
Space,
|
Space,
|
||||||
Comma,
|
Comma,
|
||||||
|
Slash,
|
||||||
|
Undecided,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ListSeparator {
|
||||||
|
#[allow(clippy::match_like_matches_macro)]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Space | Self::Undecided, Self::Space | Self::Undecided) => true,
|
||||||
|
(Self::Comma, Self::Comma) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListSeparator {
|
impl ListSeparator {
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Space => " ",
|
Self::Space | Self::Undecided => " ",
|
||||||
Self::Comma => ", ",
|
Self::Comma => ", ",
|
||||||
|
Self::Slash => " / ",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_compressed_str(self) -> &'static str {
|
pub fn as_compressed_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Space => " ",
|
Self::Space | Self::Undecided => " ",
|
||||||
Self::Comma => ",",
|
Self::Comma => ",",
|
||||||
|
Self::Slash => "/",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Space => "space",
|
Self::Space | Self::Undecided => "space",
|
||||||
Self::Comma => "comma",
|
Self::Comma => "comma",
|
||||||
|
Self::Slash => "slash",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,9 +136,17 @@ impl ListSeparator {
|
|||||||
///
|
///
|
||||||
/// This struct protects that invariant by normalizing all
|
/// This struct protects that invariant by normalizing all
|
||||||
/// underscores into hypens.
|
/// underscores into hypens.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)]
|
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)]
|
||||||
pub(crate) struct Identifier(InternedString);
|
pub(crate) struct Identifier(InternedString);
|
||||||
|
|
||||||
|
impl fmt::Debug for Identifier {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_tuple("Identifier")
|
||||||
|
.field(&self.0.to_string())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Identifier {
|
impl Identifier {
|
||||||
fn from_str(s: &str) -> Self {
|
fn from_str(s: &str) -> Self {
|
||||||
if s.contains('_') {
|
if s.contains('_') {
|
||||||
@ -130,6 +155,10 @@ impl Identifier {
|
|||||||
Identifier(InternedString::get_or_intern(s))
|
Identifier(InternedString::get_or_intern(s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_public(&self) -> bool {
|
||||||
|
!self.as_str().starts_with('-')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Identifier {
|
impl From<String> for Identifier {
|
||||||
|
112
src/context_flags.rs
Normal file
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) {
|
pub(crate) fn raw(self) -> (String, Span) {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
SassErrorKind::Raw(string, span) => (string, span),
|
SassErrorKind::Raw(string, span) => (string, span),
|
||||||
e => todo!("unable to get raw of {:?}", e),
|
e => unreachable!("unable to get raw of {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,15 +113,15 @@ impl Display for SassError {
|
|||||||
loc,
|
loc,
|
||||||
unicode,
|
unicode,
|
||||||
} => (message, loc, *unicode),
|
} => (message, loc, *unicode),
|
||||||
SassErrorKind::FromUtf8Error(s) => return writeln!(f, "Error: {}", s),
|
SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."),
|
||||||
SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
|
SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s),
|
||||||
SassErrorKind::Raw(..) => unreachable!(),
|
SassErrorKind::Raw(..) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let first_bar = if unicode { '╷' } else { '|' };
|
let first_bar = if unicode { '╷' } else { ',' };
|
||||||
let second_bar = if unicode { '│' } else { '|' };
|
let second_bar = if unicode { '│' } else { '|' };
|
||||||
let third_bar = if unicode { '│' } else { '|' };
|
let third_bar = if unicode { '│' } else { '|' };
|
||||||
let fourth_bar = if unicode { '╵' } else { '|' };
|
let fourth_bar = if unicode { '╵' } else { '\'' };
|
||||||
|
|
||||||
let line = loc.begin.line + 1;
|
let line = loc.begin.line + 1;
|
||||||
let col = loc.begin.column + 1;
|
let col = loc.begin.column + 1;
|
||||||
@ -148,7 +148,12 @@ impl Display for SassError {
|
|||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
)?;
|
)?;
|
||||||
writeln!(f, "{}{}", padding, fourth_bar)?;
|
writeln!(f, "{}{}", padding, fourth_bar)?;
|
||||||
|
|
||||||
|
if unicode {
|
||||||
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
|
writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?;
|
||||||
|
} else {
|
||||||
|
writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
603
src/evaluate/bin_op.rs
Normal file
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::{
|
||||||
use std::path::Path;
|
io::{self, Error, ErrorKind},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
/// A trait to allow replacing the file system lookup mechanisms.
|
/// A trait to allow replacing the file system lookup mechanisms.
|
||||||
///
|
///
|
||||||
@ -15,7 +17,7 @@ pub trait Fs: std::fmt::Debug {
|
|||||||
/// Returns `true` if the path exists on disk and is pointing at a regular file.
|
/// Returns `true` if the path exists on disk and is pointing at a regular file.
|
||||||
fn is_file(&self, path: &Path) -> bool;
|
fn is_file(&self, path: &Path) -> bool;
|
||||||
/// Read the entire contents of a file into a bytes vector.
|
/// Read the entire contents of a file into a bytes vector.
|
||||||
fn read(&self, path: &Path) -> Result<Vec<u8>>;
|
fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use [`std::fs`] to read any files from disk.
|
/// Use [`std::fs`] to read any files from disk.
|
||||||
@ -36,7 +38,7 @@ impl Fs for StdFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn read(&self, path: &Path) -> Result<Vec<u8>> {
|
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
|
||||||
std::fs::read(path)
|
std::fs::read(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +63,7 @@ impl Fs for NullFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn read(&self, _path: &Path) -> Result<Vec<u8>> {
|
fn read(&self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||||
Err(Error::new(
|
Err(Error::new(
|
||||||
ErrorKind::NotFound,
|
ErrorKind::NotFound,
|
||||||
"NullFs, there is no file system",
|
"NullFs, there is no file system",
|
||||||
|
@ -23,6 +23,7 @@ impl InternedString {
|
|||||||
self.resolve_ref() == ""
|
self.resolve_ref() == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: no need for unsafe here
|
||||||
pub fn resolve_ref<'a>(self) -> &'a str {
|
pub fn resolve_ref<'a>(self) -> &'a str {
|
||||||
unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) }
|
unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) }
|
||||||
}
|
}
|
||||||
|
83
src/lexer.rs
83
src/lexer.rs
@ -1,6 +1,6 @@
|
|||||||
use std::{borrow::Cow, iter::Peekable, str::Chars, sync::Arc};
|
use std::{borrow::Cow, iter::Peekable, str::Chars, sync::Arc};
|
||||||
|
|
||||||
use codemap::File;
|
use codemap::{File, Span};
|
||||||
|
|
||||||
use crate::Token;
|
use crate::Token;
|
||||||
|
|
||||||
@ -10,53 +10,70 @@ const FORM_FEED: char = '\x0C';
|
|||||||
pub(crate) struct Lexer<'a> {
|
pub(crate) struct Lexer<'a> {
|
||||||
buf: Cow<'a, [Token]>,
|
buf: Cow<'a, [Token]>,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
amt_peeked: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Lexer<'a> {
|
impl<'a> Lexer<'a> {
|
||||||
fn peek_cursor(&self) -> usize {
|
pub fn raw_text(&self, start: usize) -> String {
|
||||||
self.cursor + self.amt_peeked
|
self.buf[start..self.cursor]
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.kind)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_char_is(&self, c: char) -> bool {
|
||||||
|
matches!(self.peek(), Some(Token { kind, .. }) if kind == c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn span_from(&mut self, start: usize) -> Span {
|
||||||
|
let start = match self.buf.get(start) {
|
||||||
|
Some(start) => start.pos,
|
||||||
|
None => return self.current_span(),
|
||||||
|
};
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
let end = self.current_span();
|
||||||
|
self.cursor += 1;
|
||||||
|
|
||||||
|
start.merge(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_span(&self) -> Span {
|
||||||
|
self.buf
|
||||||
|
.get(self.cursor.saturating_sub(1))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| self.buf.last().copied().unwrap())
|
||||||
|
.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_span(&self) -> Span {
|
||||||
|
self.buf
|
||||||
|
.get(self.cursor)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| self.buf.last().copied().unwrap())
|
||||||
|
.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek(&self) -> Option<Token> {
|
pub fn peek(&self) -> Option<Token> {
|
||||||
self.buf.get(self.peek_cursor()).copied()
|
self.buf.get(self.cursor).copied()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_cursor(&mut self) {
|
|
||||||
self.amt_peeked = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn peek_next(&mut self) -> Option<Token> {
|
|
||||||
self.amt_peeked += 1;
|
|
||||||
|
|
||||||
self.peek()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Peeks the previous token without modifying the peek cursor
|
||||||
pub fn peek_previous(&mut self) -> Option<Token> {
|
pub fn peek_previous(&mut self) -> Option<Token> {
|
||||||
self.buf.get(self.peek_cursor().checked_sub(1)?).copied()
|
self.buf.get(self.cursor.checked_sub(1)?).copied()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn peek_forward(&mut self, n: usize) -> Option<Token> {
|
|
||||||
self.amt_peeked += n;
|
|
||||||
|
|
||||||
self.peek()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peeks `n` from current peeked position without modifying cursor
|
/// Peeks `n` from current peeked position without modifying cursor
|
||||||
pub fn peek_n(&self, n: usize) -> Option<Token> {
|
pub fn peek_n(&self, n: usize) -> Option<Token> {
|
||||||
self.buf.get(self.peek_cursor() + n).copied()
|
self.buf.get(self.cursor + n).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_backward(&mut self, n: usize) -> Option<Token> {
|
/// Peeks `n` behind current peeked position without modifying cursor
|
||||||
self.amt_peeked = self.amt_peeked.checked_sub(n)?;
|
pub fn peek_n_backwards(&self, n: usize) -> Option<Token> {
|
||||||
|
self.buf.get(self.cursor.checked_sub(n)?).copied()
|
||||||
self.peek()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set cursor to position and reset peek
|
/// Set cursor to position and reset peek
|
||||||
pub fn set_cursor(&mut self, cursor: usize) {
|
pub fn set_cursor(&mut self, cursor: usize) {
|
||||||
self.cursor = cursor;
|
self.cursor = cursor;
|
||||||
self.amt_peeked = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> usize {
|
pub fn cursor(&self) -> usize {
|
||||||
@ -70,7 +87,6 @@ impl<'a> Iterator for Lexer<'a> {
|
|||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.buf.get(self.cursor).copied().map(|tok| {
|
self.buf.get(self.cursor).copied().map(|tok| {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
self.amt_peeked = self.amt_peeked.saturating_sub(1);
|
|
||||||
tok
|
tok
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -122,15 +138,6 @@ impl<'a> Lexer<'a> {
|
|||||||
Lexer {
|
Lexer {
|
||||||
buf: Cow::Owned(buf),
|
buf: Cow::Owned(buf),
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
amt_peeked: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_ref(buf: &'a [Token]) -> Lexer<'a> {
|
|
||||||
Lexer {
|
|
||||||
buf: Cow::Borrowed(buf),
|
|
||||||
cursor: 0,
|
|
||||||
amt_peeked: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
286
src/lib.rs
286
src/lib.rs
@ -1,17 +1,14 @@
|
|||||||
/*! # grass
|
/*!
|
||||||
An implementation of Sass in pure rust.
|
This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS.
|
||||||
|
|
||||||
Spec progress as of 0.11.2, released on 2022-09-03:
|
|
||||||
|
|
||||||
| Passing | Failing | Total |
|
|
||||||
|---------|---------|-------|
|
|
||||||
| 4205 | 2051 | 6256 |
|
|
||||||
|
|
||||||
## Use as library
|
## Use as library
|
||||||
```
|
```
|
||||||
fn main() -> Result<(), Box<grass::Error>> {
|
fn main() -> Result<(), Box<grass::Error>> {
|
||||||
let sass = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?;
|
let css = grass::from_string(
|
||||||
assert_eq!(sass, "a b {\n color: a b;\n}\n");
|
"a { b { color: &; } }".to_owned(),
|
||||||
|
&grass::Options::default()
|
||||||
|
)?;
|
||||||
|
assert_eq!(css, "a b {\n color: a b;\n}\n");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -23,7 +20,7 @@ grass input.scss
|
|||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
#![warn(clippy::all, clippy::cargo)]
|
||||||
#![deny(missing_debug_implementations)]
|
#![deny(missing_debug_implementations)]
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::use_self,
|
clippy::use_self,
|
||||||
@ -57,200 +54,54 @@ grass input.scss
|
|||||||
clippy::items_after_statements,
|
clippy::items_after_statements,
|
||||||
// this is only available on nightly
|
// this is only available on nightly
|
||||||
clippy::unnested_or_patterns,
|
clippy::unnested_or_patterns,
|
||||||
|
clippy::uninlined_format_args,
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::cast_precision_loss,
|
||||||
|
clippy::float_cmp,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::comparison_chain,
|
||||||
|
clippy::bool_to_int_with_if,
|
||||||
)]
|
)]
|
||||||
#![cfg_attr(feature = "profiling", inline(never))]
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use parse::{CssParser, SassParser, StylesheetParser};
|
||||||
|
use serializer::Serializer;
|
||||||
#[cfg(feature = "wasm-exports")]
|
#[cfg(feature = "wasm-exports")]
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
pub(crate) use beef::lean::Cow;
|
|
||||||
|
|
||||||
use codemap::CodeMap;
|
use codemap::CodeMap;
|
||||||
|
|
||||||
pub use crate::error::{
|
pub use crate::error::{
|
||||||
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
|
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
|
||||||
};
|
};
|
||||||
pub use crate::fs::{Fs, NullFs, StdFs};
|
pub use crate::fs::{Fs, NullFs, StdFs};
|
||||||
pub(crate) use crate::token::Token;
|
pub use crate::options::{InputSyntax, Options, OutputStyle};
|
||||||
use crate::{
|
pub(crate) use crate::{context_flags::ContextFlags, token::Token};
|
||||||
builtin::modules::{ModuleConfig, Modules},
|
use crate::{evaluate::Visitor, lexer::Lexer, parse::ScssParser};
|
||||||
lexer::Lexer,
|
|
||||||
output::{AtRuleContext, Css},
|
|
||||||
parse::{
|
|
||||||
common::{ContextFlags, NeverEmptyVec},
|
|
||||||
Parser,
|
|
||||||
},
|
|
||||||
scope::{Scope, Scopes},
|
|
||||||
selector::{ExtendedSelector, Extender, SelectorList},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod args;
|
mod ast;
|
||||||
mod atrule;
|
|
||||||
mod builtin;
|
mod builtin;
|
||||||
mod color;
|
mod color;
|
||||||
mod common;
|
mod common;
|
||||||
|
mod context_flags;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod evaluate;
|
||||||
mod fs;
|
mod fs;
|
||||||
mod interner;
|
mod interner;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
mod output;
|
mod options;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod scope;
|
|
||||||
mod selector;
|
mod selector;
|
||||||
mod style;
|
mod serializer;
|
||||||
mod token;
|
mod token;
|
||||||
mod unit;
|
mod unit;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod value;
|
mod value;
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum OutputStyle {
|
|
||||||
/// The default style, this mode writes each
|
|
||||||
/// selector and declaration on its own line.
|
|
||||||
Expanded,
|
|
||||||
/// Ideal for release builds, this mode removes
|
|
||||||
/// as many extra characters as possible and
|
|
||||||
/// writes the entire stylesheet on a single line.
|
|
||||||
Compressed,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for Sass compilation
|
|
||||||
///
|
|
||||||
/// The simplest usage is `grass::Options::default()`;
|
|
||||||
/// however, a builder pattern is also exposed to offer
|
|
||||||
/// more control.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Options<'a> {
|
|
||||||
fs: &'a dyn Fs,
|
|
||||||
style: OutputStyle,
|
|
||||||
load_paths: Vec<&'a Path>,
|
|
||||||
allows_charset: bool,
|
|
||||||
unicode_error_messages: bool,
|
|
||||||
quiet: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Options<'_> {
|
|
||||||
#[inline]
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
fs: &StdFs,
|
|
||||||
style: OutputStyle::Expanded,
|
|
||||||
load_paths: Vec::new(),
|
|
||||||
allows_charset: true,
|
|
||||||
unicode_error_messages: true,
|
|
||||||
quiet: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Options<'a> {
|
|
||||||
/// This option allows you to control the file system that Sass will see.
|
|
||||||
///
|
|
||||||
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
|
|
||||||
/// allowing direct, unfettered access to the local file system.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
|
|
||||||
self.fs = fs;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `grass` currently offers 2 different output styles
|
|
||||||
///
|
|
||||||
/// - `OutputStyle::Expanded` writes each selector and declaration on its own line.
|
|
||||||
/// - `OutputStyle::Compressed` removes as many extra characters as possible
|
|
||||||
/// and writes the entire stylesheet on a single line.
|
|
||||||
///
|
|
||||||
/// By default, output is expanded.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub const fn style(mut self, style: OutputStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This flag tells Sass not to emit any warnings
|
|
||||||
/// when compiling. By default, Sass emits warnings
|
|
||||||
/// when deprecated features are used or when the
|
|
||||||
/// `@warn` rule is encountered. It also silences the
|
|
||||||
/// `@debug` rule.
|
|
||||||
///
|
|
||||||
/// By default, this value is `false` and warnings are emitted.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub const fn quiet(mut self, quiet: bool) -> Self {
|
|
||||||
self.quiet = quiet;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All Sass implementations allow users to provide
|
|
||||||
/// load paths: paths on the filesystem that Sass
|
|
||||||
/// will look in when locating modules. For example,
|
|
||||||
/// if you pass `node_modules/susy/sass` as a load path,
|
|
||||||
/// you can use `@import "susy"` to load `node_modules/susy/sass/susy.scss`.
|
|
||||||
///
|
|
||||||
/// Imports will always be resolved relative to the current
|
|
||||||
/// file first, though. Load paths will only be used if no
|
|
||||||
/// relative file exists that matches the module's URL. This
|
|
||||||
/// ensures that you can't accidentally mess up your relative
|
|
||||||
/// imports when you add a new library.
|
|
||||||
///
|
|
||||||
/// This method will append a single path to the list.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub fn load_path(mut self, path: &'a Path) -> Self {
|
|
||||||
self.load_paths.push(path);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append multiple loads paths
|
|
||||||
///
|
|
||||||
/// Note that this method does *not* remove existing load paths
|
|
||||||
///
|
|
||||||
/// See [`Options::load_path`](Options::load_path) for more information about load paths
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub fn load_paths(mut self, paths: &'a [&'a Path]) -> Self {
|
|
||||||
self.load_paths.extend_from_slice(paths);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This flag tells Sass whether to emit a `@charset`
|
|
||||||
/// declaration or a UTF-8 byte-order mark.
|
|
||||||
///
|
|
||||||
/// By default, Sass will insert either a `@charset`
|
|
||||||
/// declaration (in expanded output mode) or a byte-order
|
|
||||||
/// mark (in compressed output mode) if the stylesheet
|
|
||||||
/// contains any non-ASCII characters.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub const fn allows_charset(mut self, allows_charset: bool) -> Self {
|
|
||||||
self.allows_charset = allows_charset;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This flag tells Sass only to emit ASCII characters as
|
|
||||||
/// part of error messages.
|
|
||||||
///
|
|
||||||
/// By default Sass will emit non-ASCII characters for
|
|
||||||
/// these messages.
|
|
||||||
///
|
|
||||||
/// This flag does not affect the CSS output.
|
|
||||||
#[must_use]
|
|
||||||
#[inline]
|
|
||||||
pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self {
|
|
||||||
self.unicode_error_messages = unicode_error_messages;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_compressed(&self) -> bool {
|
|
||||||
matches!(self.style, OutputStyle::Compressed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
|
fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
|
||||||
let (message, span) = err.raw();
|
let (message, span) = err.raw();
|
||||||
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
|
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
|
||||||
@ -260,34 +111,59 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
|
|||||||
let mut map = CodeMap::new();
|
let mut map = CodeMap::new();
|
||||||
let file = map.add_file(file_name.to_owned(), input);
|
let file = map.add_file(file_name.to_owned(), input);
|
||||||
let empty_span = file.span.subspan(0, 0);
|
let empty_span = file.span.subspan(0, 0);
|
||||||
|
let lexer = Lexer::new_from_file(&file);
|
||||||
|
|
||||||
let stmts = Parser {
|
let path = Path::new(file_name);
|
||||||
toks: &mut Lexer::new_from_file(&file),
|
|
||||||
map: &mut map,
|
let input_syntax = options
|
||||||
path: file_name.as_ref(),
|
.input_syntax
|
||||||
scopes: &mut Scopes::new(),
|
.unwrap_or_else(|| InputSyntax::for_path(path));
|
||||||
global_scope: &mut Scope::new(),
|
|
||||||
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new(
|
let stylesheet = match input_syntax {
|
||||||
empty_span,
|
InputSyntax::Scss => {
|
||||||
))),
|
ScssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||||
span_before: empty_span,
|
|
||||||
content: &mut Vec::new(),
|
|
||||||
flags: ContextFlags::empty(),
|
|
||||||
at_root: true,
|
|
||||||
at_root_has_selector: false,
|
|
||||||
extender: &mut Extender::new(empty_span),
|
|
||||||
content_scopes: &mut Scopes::new(),
|
|
||||||
options,
|
|
||||||
modules: &mut Modules::default(),
|
|
||||||
module_config: &mut ModuleConfig::default(),
|
|
||||||
}
|
}
|
||||||
.parse()
|
InputSyntax::Sass => {
|
||||||
|
SassParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||||
|
}
|
||||||
|
InputSyntax::Css => {
|
||||||
|
CssParser::new(lexer, &mut map, options, empty_span, file_name.as_ref()).__parse()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stylesheet = match stylesheet {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut visitor = Visitor::new(path, options, &mut map, empty_span);
|
||||||
|
match visitor.visit_stylesheet(stylesheet) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)),
|
||||||
|
}
|
||||||
|
let stmts = visitor.finish();
|
||||||
|
|
||||||
|
let mut serializer = Serializer::new(options, &map, false, empty_span);
|
||||||
|
|
||||||
|
let mut prev_was_group_end = false;
|
||||||
|
let mut prev_requires_semicolon = false;
|
||||||
|
for stmt in stmts {
|
||||||
|
if stmt.is_invisible() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_group_end = stmt.is_group_end();
|
||||||
|
let requires_semicolon = Serializer::requires_semicolon(&stmt);
|
||||||
|
|
||||||
|
serializer
|
||||||
|
.visit_group(stmt, prev_was_group_end, prev_requires_semicolon)
|
||||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
|
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
|
||||||
|
|
||||||
Css::from_stmts(stmts, AtRuleContext::None, options.allows_charset)
|
prev_was_group_end = is_group_end;
|
||||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
|
prev_requires_semicolon = requires_semicolon;
|
||||||
.pretty_print(&map, options.style)
|
}
|
||||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
|
|
||||||
|
Ok(serializer.finish(prev_requires_semicolon))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile CSS from a path
|
/// Compile CSS from a path
|
||||||
@ -300,8 +176,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
|
|||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(feature = "profiling", inline(never))]
|
|
||||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
#[inline]
|
||||||
pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||||
from_string_with_file_name(
|
from_string_with_file_name(
|
||||||
String::from_utf8(options.fs.read(Path::new(p))?)?,
|
String::from_utf8(options.fs.read(Path::new(p))?)?,
|
||||||
@ -319,8 +195,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
|||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(feature = "profiling", inline(never))]
|
|
||||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
#[inline]
|
||||||
pub fn from_string(input: String, options: &Options) -> Result<String> {
|
pub fn from_string(input: String, options: &Options) -> Result<String> {
|
||||||
from_string_with_file_name(input, "stdin", options)
|
from_string_with_file_name(input, "stdin", options)
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ arg_enum! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "profiling", inline(never))]
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let matches = App::new("grass")
|
let matches = App::new("grass")
|
||||||
.setting(AppSettings::ColoredHelp)
|
.setting(AppSettings::ColoredHelp)
|
||||||
@ -144,6 +143,12 @@ fn main() -> std::io::Result<()> {
|
|||||||
.hidden(true)
|
.hidden(true)
|
||||||
.help("Whether to use terminal colors for messages.")
|
.help("Whether to use terminal colors for messages.")
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("VERBOSE")
|
||||||
|
.long("verbose")
|
||||||
|
.hidden(true)
|
||||||
|
.help("Print all deprecation warnings even when they're repetitive.")
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("NO_UNICODE")
|
Arg::with_name("NO_UNICODE")
|
||||||
.long("no-unicode")
|
.long("no-unicode")
|
||||||
|
193
src/options.rs
Normal file
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 std::fmt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, token::Token};
|
||||||
atrule::keyframes::{Keyframes, KeyframesSelector},
|
|
||||||
error::SassResult,
|
|
||||||
lexer::Lexer,
|
|
||||||
parse::Stmt,
|
|
||||||
Token,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{common::ContextFlags, Parser};
|
use super::BaseParser;
|
||||||
|
|
||||||
impl fmt::Display for KeyframesSelector {
|
impl fmt::Display for KeyframesSelector {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
@ -20,201 +14,128 @@ impl fmt::Display for KeyframesSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct KeyframesSelectorParser<'a, 'b, 'c> {
|
pub(crate) struct KeyframesSelectorParser<'a> {
|
||||||
parser: &'a mut Parser<'b, 'c>,
|
toks: Lexer<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, 'c> KeyframesSelectorParser<'a, 'b, 'c> {
|
impl<'a> BaseParser<'a> for KeyframesSelectorParser<'a> {
|
||||||
pub fn new(parser: &'a mut Parser<'b, 'c>) -> Self {
|
fn toks(&self) -> &Lexer<'a> {
|
||||||
Self { parser }
|
&self.toks
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> {
|
fn toks_mut(&mut self) -> &mut Lexer<'a> {
|
||||||
|
&mut self.toks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> KeyframesSelectorParser<'a> {
|
||||||
|
pub fn new(toks: Lexer<'a>) -> KeyframesSelectorParser<'a> {
|
||||||
|
KeyframesSelectorParser { toks }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_keyframes_selector(&mut self) -> SassResult<Vec<KeyframesSelector>> {
|
||||||
let mut selectors = Vec::new();
|
let mut selectors = Vec::new();
|
||||||
self.parser.whitespace_or_comment();
|
loop {
|
||||||
while let Some(tok) = self.parser.toks.peek() {
|
self.whitespace()?;
|
||||||
match tok.kind {
|
if self.looking_at_identifier() {
|
||||||
't' | 'T' => {
|
if self.scan_identifier("to", false)? {
|
||||||
let mut ident = self.parser.parse_identifier()?;
|
|
||||||
ident.node.make_ascii_lowercase();
|
|
||||||
if ident.node == "to" {
|
|
||||||
selectors.push(KeyframesSelector::To);
|
selectors.push(KeyframesSelector::To);
|
||||||
} else {
|
} else if self.scan_identifier("from", false)? {
|
||||||
return Err(("Expected \"to\" or \"from\".", tok.pos).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'f' | 'F' => {
|
|
||||||
let mut ident = self.parser.parse_identifier()?;
|
|
||||||
ident.node.make_ascii_lowercase();
|
|
||||||
if ident.node == "from" {
|
|
||||||
selectors.push(KeyframesSelector::From);
|
selectors.push(KeyframesSelector::From);
|
||||||
} else {
|
} else {
|
||||||
return Err(("Expected \"to\" or \"from\".", tok.pos).into());
|
return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
'0'..='9' => {
|
|
||||||
let mut num = self.parser.parse_whole_number();
|
|
||||||
|
|
||||||
if let Some(Token { kind: '.', .. }) = self.parser.toks.peek() {
|
|
||||||
self.parser.toks.next();
|
|
||||||
num.push('.');
|
|
||||||
num.push_str(&self.parser.parse_whole_number());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.parser.expect_char('%')?;
|
|
||||||
|
|
||||||
selectors.push(KeyframesSelector::Percent(num.into_boxed_str()));
|
|
||||||
}
|
|
||||||
'{' => break,
|
|
||||||
'\\' => todo!("escaped chars in @keyframes selector"),
|
|
||||||
_ => return Err(("Expected \"to\" or \"from\".", tok.pos).into()),
|
|
||||||
}
|
|
||||||
self.parser.whitespace_or_comment();
|
|
||||||
if let Some(Token { kind: ',', .. }) = self.parser.toks.peek() {
|
|
||||||
self.parser.toks.next();
|
|
||||||
self.parser.whitespace_or_comment();
|
|
||||||
} else {
|
} else {
|
||||||
|
selectors.push(self.parse_percentage_selector()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.whitespace()?;
|
||||||
|
|
||||||
|
if !self.scan_char(',') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(selectors)
|
Ok(selectors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_percentage_selector(&mut self) -> SassResult<KeyframesSelector> {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
|
||||||
|
if self.scan_char('+') {
|
||||||
|
buffer.push('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Parser<'a, 'b> {
|
if !matches!(
|
||||||
fn parse_keyframes_name(&mut self) -> SassResult<String> {
|
self.toks.peek(),
|
||||||
let mut name = String::new();
|
Some(Token {
|
||||||
self.whitespace_or_comment();
|
kind: '0'..='9' | '.',
|
||||||
while let Some(tok) = self.toks.next() {
|
..
|
||||||
match tok.kind {
|
|
||||||
'#' => {
|
|
||||||
if self.consume_char_if_exists('{') {
|
|
||||||
name.push_str(&self.parse_interpolation_as_string()?);
|
|
||||||
} else {
|
|
||||||
name.push('#');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' ' | '\n' | '\t' => {
|
|
||||||
self.whitespace();
|
|
||||||
name.push(' ');
|
|
||||||
}
|
|
||||||
'{' => {
|
|
||||||
// todo: we can avoid the reallocation by trimming before emitting
|
|
||||||
// (in `output.rs`)
|
|
||||||
return Ok(name.trim().to_owned());
|
|
||||||
}
|
|
||||||
_ => name.push(tok.kind),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(("expected \"{\".", self.span_before).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn parse_keyframes_selector(
|
|
||||||
&mut self,
|
|
||||||
mut string: String,
|
|
||||||
) -> SassResult<Vec<KeyframesSelector>> {
|
|
||||||
let mut span = if let Some(tok) = self.toks.peek() {
|
|
||||||
tok.pos()
|
|
||||||
} else {
|
|
||||||
return Err(("expected \"{\".", self.span_before).into());
|
|
||||||
};
|
|
||||||
|
|
||||||
self.span_before = span;
|
|
||||||
|
|
||||||
while let Some(tok) = self.toks.next() {
|
|
||||||
span = span.merge(tok.pos());
|
|
||||||
match tok.kind {
|
|
||||||
'#' => {
|
|
||||||
if self.consume_char_if_exists('{') {
|
|
||||||
string.push_str(
|
|
||||||
&self
|
|
||||||
.parse_interpolation()?
|
|
||||||
.to_css_string(span, self.options.is_compressed())?,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
string.push('#');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
',' => {
|
|
||||||
while let Some(c) = string.pop() {
|
|
||||||
if c == ' ' || c == ',' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
string.push(c);
|
|
||||||
string.push(',');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'/' => {
|
|
||||||
if self.toks.peek().is_none() {
|
|
||||||
return Err(("Expected selector.", tok.pos()).into());
|
|
||||||
}
|
|
||||||
self.parse_comment()?;
|
|
||||||
self.whitespace();
|
|
||||||
string.push(' ');
|
|
||||||
}
|
|
||||||
'{' => {
|
|
||||||
let sel_toks: Vec<Token> =
|
|
||||||
string.chars().map(|x| Token::new(span, x)).collect();
|
|
||||||
|
|
||||||
let selector = KeyframesSelectorParser::new(&mut Parser {
|
|
||||||
toks: &mut Lexer::new(sel_toks),
|
|
||||||
map: self.map,
|
|
||||||
path: self.path,
|
|
||||||
scopes: self.scopes,
|
|
||||||
global_scope: self.global_scope,
|
|
||||||
super_selectors: self.super_selectors,
|
|
||||||
span_before: self.span_before,
|
|
||||||
content: self.content,
|
|
||||||
flags: self.flags,
|
|
||||||
at_root: self.at_root,
|
|
||||||
at_root_has_selector: self.at_root_has_selector,
|
|
||||||
extender: self.extender,
|
|
||||||
content_scopes: self.content_scopes,
|
|
||||||
options: self.options,
|
|
||||||
modules: self.modules,
|
|
||||||
module_config: self.module_config,
|
|
||||||
})
|
})
|
||||||
.parse_keyframes_selector()?;
|
) {
|
||||||
|
return Err(("Expected number.", self.toks.current_span()).into());
|
||||||
return Ok(selector);
|
|
||||||
}
|
}
|
||||||
c => string.push(c),
|
|
||||||
|
while matches!(
|
||||||
|
self.toks.peek(),
|
||||||
|
Some(Token {
|
||||||
|
kind: '0'..='9',
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
buffer.push(self.toks.next().unwrap().kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.scan_char('.') {
|
||||||
|
buffer.push('.');
|
||||||
|
|
||||||
|
while matches!(
|
||||||
|
self.toks.peek(),
|
||||||
|
Some(Token {
|
||||||
|
kind: '0'..='9',
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
buffer.push(self.toks.next().unwrap().kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(("expected \"{\".", span).into())
|
if self.scan_ident_char('e', false)? {
|
||||||
|
buffer.push('e');
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
self.toks.peek(),
|
||||||
|
Some(Token {
|
||||||
|
kind: '+' | '-',
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
buffer.push(self.toks.next().unwrap().kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> {
|
if !matches!(
|
||||||
if self.flags.in_function() {
|
self.toks.peek(),
|
||||||
return Err(("This at-rule is not allowed here.", self.span_before).into());
|
Some(Token {
|
||||||
|
kind: '0'..='9',
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return Err(("Expected digit.", self.toks.current_span()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = self.parse_keyframes_name()?;
|
while matches!(
|
||||||
|
self.toks.peek(),
|
||||||
self.whitespace();
|
Some(Token {
|
||||||
|
kind: '0'..='9',
|
||||||
let body = Parser {
|
..
|
||||||
toks: self.toks,
|
})
|
||||||
map: self.map,
|
) {
|
||||||
path: self.path,
|
buffer.push(self.toks.next().unwrap().kind);
|
||||||
scopes: self.scopes,
|
}
|
||||||
global_scope: self.global_scope,
|
}
|
||||||
super_selectors: self.super_selectors,
|
|
||||||
span_before: self.span_before,
|
self.expect_char('%')?;
|
||||||
content: self.content,
|
|
||||||
flags: self.flags | ContextFlags::IN_KEYFRAMES,
|
Ok(KeyframesSelector::Percent(buffer.into_boxed_str()))
|
||||||
at_root: false,
|
|
||||||
at_root_has_selector: self.at_root_has_selector,
|
|
||||||
extender: self.extender,
|
|
||||||
content_scopes: self.content_scopes,
|
|
||||||
options: self.options,
|
|
||||||
modules: self.modules,
|
|
||||||
module_config: self.module_config,
|
|
||||||
}
|
|
||||||
.parse_stmt()?;
|
|
||||||
|
|
||||||
Ok(Stmt::Keyframes(Box::new(Keyframes { rule, name, body })))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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