pax_global_header00006660000000000000000000000064146537472000014522gustar00rootroot0000000000000052 comment=e0bb9e2eabfc3a58e42b03089cd7b22c68d09d0b grass-0.13.4/000077500000000000000000000000001465374720000127265ustar00rootroot00000000000000grass-0.13.4/.github/000077500000000000000000000000001465374720000142665ustar00rootroot00000000000000grass-0.13.4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001465374720000164515ustar00rootroot00000000000000grass-0.13.4/.github/ISSUE_TEMPLATE/incorrect-sass-output.md000066400000000000000000000006341465374720000232730ustar00rootroot00000000000000--- name: Incorrect Sass Output about: `grass` and `dart-sass` differ in output or `grass` reports and error for a valid style sheet title: '' labels: bug assignees: connorskees --- **Failing Sass**: ``` a { color: red; } ``` **`grass` Output**: ``` a { color: red; } ``` **`dart-sass` Output**: ``` a { color: red; } ``` grass-0.13.4/.github/workflows/000077500000000000000000000000001465374720000163235ustar00rootroot00000000000000grass-0.13.4/.github/workflows/build_wasm.yml000066400000000000000000000015431465374720000211770ustar00rootroot00000000000000name: 'Build WebAssembly' on: push: tags: [ '*' ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Install wasm-pack' run: | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh export PATH="$HOME/.cargo/bin:$PATH" - name: 'Build WebAssembly' run: | rustup target add wasm32-unknown-unknown pushd ./crates/lib wasm-pack build --release --target web --out-name index popd mkdir -p artifacts find crates/lib/pkg -type f \( -name "*.js" -o -name "*.d.ts" -o -name "*.wasm" \) -exec cp {} artifacts \; - name: 'Upload Artifacts' uses: actions/upload-artifact@v4 with: name: 'wasm' path: artifacts/* grass-0.13.4/.github/workflows/tests.yml000066400000000000000000000054451465374720000202200ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: jobs: tests: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - uses: dtolnay/rust-toolchain@master with: toolchain: "1.70.0" - name: version info run: rustc --version; cargo --version; - name: Run all tests run: cargo test --features=macro fmt: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - uses: dtolnay/rust-toolchain@master with: toolchain: "1.70.0" - run: | rustup component add rustfmt cargo fmt --all -- --check clippy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - uses: dtolnay/rust-toolchain@master with: toolchain: "1.70.0" - run: | rustup component add clippy cargo clippy --features=macro -- -D warnings bootstrap: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Build run: cargo build - name: Install dart-sass 1.54.3 run: | wget https://github.com/sass/dart-sass/releases/download/1.54.3/dart-sass-1.54.3-linux-x64.tar.gz tar -xzvf dart-sass-1.54.3-linux-x64.tar.gz - name: Install bootstrap run: git clone --depth=1 --branch v5.0.2 https://github.com/twbs/bootstrap.git - name: Verify unchanged output run: | ./target/debug/grass bootstrap/scss/bootstrap.scss > grass-output.css ./dart-sass/sass bootstrap/scss/bootstrap.scss > dart-sass-output.css if [[ $(diff -u grass-output.css dart-sass-output.css) ]]; then echo "Differences found" diff -u grass-output.css dart-sass-output.css exit 1 else echo "No differences found" fi # sass-spec: # continue-on-error: true # runs-on: ubuntu-latest # steps: # - name: Checkout # uses: actions/checkout@master # - uses: dtolnay/rust-toolchain@master # with: # toolchain: stable # - name: version info # run: rustc --version; cargo --version; # - name: Build binary # run: cargo b --release # - name: Get sass-spec # run: git submodule init && git submodule update # - name: Install whatever Ruby needs # run: | # sudo apt-get install libncurses5-dev libncursesw5-dev # sudo gem install bundler # cd sass-spec && bundler install # - name: Run Sass spec # run: ./sass-spec/sass-spec.rb -c './target/release/grass' grass-0.13.4/.gitignore000066400000000000000000000006251465374720000147210ustar00rootroot00000000000000*.s[ac]ss *.css !input.scss !crates/static/_index.scss # devops /target coverage pkg flamegraph.svg perf* dhat* *.tar.gz # editor specific .idea .vscode # automation tools *.py *.sh *.cmd *.exe # common Sass frameworks that may wish to live alongside grass for testing susy bulma* bootstrap* materialize uikit bourbon foundation-sites sassline true dart-sass sass-fairy duomo ibm-cloud-cognitive pico grass-0.13.4/.gitmodules000066400000000000000000000001231465374720000150770ustar00rootroot00000000000000[submodule "sass-spec"] path = sass-spec url = https://github.com/sass/sass-spec grass-0.13.4/CHANGELOG.md000066400000000000000000000324271465374720000145470ustar00rootroot00000000000000 # 0.13.4 - support `...$keys` argument to `map-has-key(..)`/`map.has-key(..)` - parse [aliased colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color#description) (e.g. `cyan` for `aqua`) as colors rather than identifiers # 0.13.3 - implement builtin string-module function `string.split(..)` (#96) by @xpe - implement functionality for intercepting logs (#93) by cryocz # 0.13.2 - update rustix dependency to silence security warning - fix @forward statement altering the scope of the forwarded module (#85) by @kketch - bump MSRV to 1.70.0 # 0.13.1 - update `clap` dependency to 4.x.x to silence `atty` security warning - bump MSRV to 1.64.0 for new `clap` version - fix bug in which `--no-charset` flag wasn't respected # 0.13.0 - fix various module system bugs when combined with `@import`. this is potentially breaking in rare cases where users were relying on the incorrect behavior - expose more AST internals in `grass_compiler` - allow building docs with stable/beta rust compiler - support `...$keys` argument to `map-get(..)`/`map.get(..)` (#83) # 0.12.4 - implement builtin map-module functions `map.deep-merge(..)` and `map.deep-remove(..)` # 0.12.3 No visible changes for users of the `grass` crate Exposes more internals of the `grass_compiler` crate, allowing for custom functions implemented in rust to be accessed from Sass. # 0.12.2 - implement an import cache, significantly improving the performance of certain pathological cases - slash lists can be compared using `==` - resolve rounding errors for extremely large numbers - potentially breaking bug fixes in certain color functions - `color.hwb(..)` no longer allows whiteness or blackness values outside the bounds 0% to 100% - `scale-color(..)` no longer allows the `$hue` argument. previously it was ignored - `scale-color(..)`, `change-color(..)`, and `adjust-color(..)` no longer allow invalid combinations of arguments or unknown named arguments - many functions that accept hues now convert other angle units (`rad`, `grad`, `turn`) to `deg`. previously the unit was ignored - improve compressed output of selectors containing newlines and `rgba(..)` colors - improve resolution of imports containing explicit file extensions, e.g. `@import "foo.scss"` - fix bug in which whitespace was not emitted between `+` or `-` inside calc for compressed output ([#71](https://github.com/connorskees/grass/pull/71) by @ModProg) # 0.12.1 - add `grass::include!` macro to make it easier to include CSS at compile time - various optimizations improving the bootstrap benchmark by ~30% and the bulma benchmark by ~15% - improve error message for complex units in calculations - more accurate formatting of named arguments in arglists when passed to `inspect(..)` - more accurate formatting of nested lists with different separators when passed to `inspect(..)` - support `$whiteness` and `$blackness` as arguments to `scale-color(..)` - more accurate list separator from `join(..)` - resolve unicode edge cases in `str-index(..)` - more robust support for `@forward` prefixes - allow strings as the first argument to `call(..)` - bug fix: add back support for the `$css` argument to `get-function(..)`. regressed in 0.12.0 # 0.12.0 - 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() # 0.11.2 - make `grass::Error` a `Send` type - expose more internals of `grass::Error`, allowing for custom formatting - fix WASM builds # 0.11.1 - fix load path bug in which paths were searched for relative to the SCSS file, not the executable (#57) # 0.11.0 - `fs` option added to allow interception and reimplementation of all file system operations (such as imports) - `wasm` feature renamed to/replaced with `wasm-exports`, which no longer materially alters the API: `from_path` is reinstated, and `from_string` once again returns the full error type; but the WASM export `from_string` (which returns a string error) is now a new function `from_string_js`. (It was renamed from `wasm` to `wasm-exports` because the name was misleading; Rust code that uses grass doesn’t need this feature, it’s solely to get this `from_string` WASM export.) # 0.10.8 - bugfix: properly emit the number `0` in compressed mode (#53) # 0.10.7 - special case plain CSS fn `clamp` - support more uses of plain CSS fns inside `rgb`/`rgba`/`hsl`/`hsla` - better support for `@at-root` at the toplevel and inside media queries - bugfixes for the module system - more robust handling of load paths that are directories # 0.10.6 - **feature complete, byte-for-byte support for bootstrap** - add bootstrap v5.0.2 to ci - run script to verify output against the last 2,500 commits to bootstrap - feature complete `min`/`max` support -- special functions and `min`/`max` are now allowed as arguments - removed dependency on `peekmore`, which sped up parsing and simplified lookahead - emit comments inside the `@if` rule body - fix bug in `hue(...)` function in which the value would be incorrect when the `red` channel was the highest and the green channel was lower than the blue channel - no longer round output from `saturation(...)` function - improve handling of newlines for `@media`, `@supports`, `@at-root`, placeholder selectors, unrelated style rules, and unknown @-rules - arglists can be equal to comma separated lists - throw error for invalid uses of `@charset` - more robustly parse `@else if`, allowing escaped and uppercase characters - resolve two `@extend` bugs -- one in which we would incorrectly emit `a b, a > b` as a selector, even though `a b` is a superselector of `a > b`, and a feature called "three-level extend loop", in which a stylesheet where `a` extends `b`, `b` extends `c`, and `c` extends `a` would fail to include all 3 selectors in certain places - support compressed values for comma separated lists and numbers - more robustly parse unknown @-rules # 0.10.5 - support compressed output - support new builtin functions `math.div`, `map.set` - support the HWB colorspace and builtin functions `color.hwb`, `color.blackness`, `color.whiteness` - `:is` pseudo selector is now considered an alias of `:matches` in `@extend` - support `$keys...` argument in `map.merge` - `%` now implements the modulo operation, rather than finding the remainder. this largely affects negative numbers - fix parsing bug in which `/***/` in a selector would miss the closing `/` # 0.10.4 - plain css `invert(..)` accepts numbers with any unit - plain css imports (e.g. `@import url(foo)` or `@import "foo.css"`) are now emitted at the top of documents # 0.10.3 - hyphen followed by interpolation is not treated as subtraction, e.g. `10-#{10}` => `10 -10` rather than `0` - function arguments do not affect variables in outer scopes (fixes [#37](https://github.com/connorskees/grass/issues/37)) - improve error messages for NaN with units passed to builtin functions # 0.10.2 - use `std::fs::OpenOptions` to open files ([#35](https://github.com/connorskees/grass/pull/35) by [@MidasLamb](https://github.com/MidasLamb)) # 0.10.1 - **implement `@use` and the module system** - support the filter syntax for function arguments, e.g. `alpha(opacity=1)` - disallow certain at-rules in functions, resolving several panics - allow vendor-prefixed special CSS functions, e.g. `-webkit-calc(...)` - allow decimal percent selectors inside `@keyframes` - allow vendor-prefixed `@keyframes` - resolve parsing bug for maps involving silent comments - allow escaped `!` in selectors - allow multiline comments in functions - resolve several panics on malformed input when parsing bracketed lists - support NaN in all contexts - add support for unicode ranges - recognize plain CSS imports beginning with `//`, e.g. `@import "//fonts.googleapis.com/css?family=Droid+Sans";` - resolve integer overflows in `@for` when bounds were equal to `i32::MIN` and `i32::MAX` - allow quoted strings in default function arguments # 0.10.0 - bugfixes for `@media` query regressions - bugfixes for maps, arglists, and `@each` - implement string interning for identifiers and style properties - implement spec-compliant variable scoping - emit `@import` when importing `url(...)` or `*.css` - resolve all panics for malformed `@import` - various optimizations that now allow us to compile bootstrap 10% faster than `libsass` - errors inside builtin functions use `inspect` to print values - bugfixes for color and map equality (e.g. `red` == `#ff0000`) - hide unimplemented command line flags - implement CLI options for `--quiet`, `--load-path` ([#22](https://github.com/connorskees/grass/pull/22) by @JosephLing), `--no-charset`, `--stdin`, and `--no-unicode` - use unicode characters in error messages by default - allow comma separated `@import` statements ([#23](https://github.com/connorskees/grass/pull/23) by @JosephLing) - implement and correctly parse `!optional` in `@extend` - lazily evaluate `!default` variable values - disallow interpolation in mixin and function names - improve parsing for `@supports` and unknown at-rules ## Breaking - functions now take an `Options` struct # 0.9.5 A small release fixing potential build issues and improving documentation. This release is not published to NPM due to [a bug](https://github.com/rustwasm/wasm-pack/issues/837) in `wasm-pack`. # 0.9.4 - implement `@keyframes` - don't strip newlines following comments in selectors # 0.9.3 - fix parsing bugs for empty bracketed lists - partially implement inverse units - remove all remaining `todo!()`s from binary and unary ops - parse keywords case sensitively - various optimizations that make bulma about _6x faster_ to compile # 0.9.2 - implement builtin functions `min` and `max` - bugfixes for `@extend` and `selector-unify` - allow `@content` to take arguments - bugfixes for `@content`, for example it will no longer infinitely recurse for chained mixins - better support queries in `@media` - bugfixes for `@media` - add support for splats, e.g. `rgba([1, 2, 3, 4]...)` - resolve a number of parsing bugs for `@for`, variable declarations, selectors, and maps - completely rewrite how styles are evaluated, allowing short circuiting of values like `false and unit(foo)` and `if(true, foo, unit(foo)` # 0.9.1 This release is largely focused on `@extend`, but it also resolves some regressions resulting from the new parser. - **implement `@extend`** - properly document new API - MVP implementation of `@supports` - fix regression in which `@at-root` would panic when placed after a ruleset - fix regression related to `@mixin` and `@function` scoping when combined with outer, local variables - remove most remaining `unwrap`s that could result in a panic # 0.9.0 This release is focused on setting up the groundwork for implementing `@extend` as well as being able to compile Bootstrap. - implement all builtin selector functions - `selector-append` - `selector-extend` - `selector-nest` - `selector-parse` - `selector-replace` - `selector-unify` - `simple-selectors` - `is-superselector` - implement builtin function `content-exists` - allow `@import`, `@warn`, and `@debug` in all contexts, such as inside `@mixin` - refactor control flow evaluation, resolving some issues blocking Bootstrap #### Breaking Changes - remove the `StyleSheet` struct in favor of freestanding functions, `from_string` and `from_path` # 0.8.3 This release is largely focused on performance and robustness - implement smallint optimization for numbers, making some benchmarks 50% faster - remove `bimap` as a dependency for storing named colors in favor of an ad hoc, more specialized data structure - remove _dozens_ of panics on malformed input - use `beef::Cow` instead of `std::borrow::Cow` - increase code coverage to 80% # 0.8.2 This release contains significant (>10x) improvements for WASM speed. Performance is now comparable to libsass bindings with `node-sass` as well as `dart-sass` with dart2js. It is, however, roughly 4x slower than native `grass`. grass-0.13.4/Cargo.lock000066400000000000000000000347621465374720000146470ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "ahash" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "anstream" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "codemap" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "grass" version = "0.13.4" dependencies = [ "clap", "getrandom", "grass_compiler", "include_sass", "paste", "tempfile", "wasm-bindgen", ] [[package]] name = "grass_compiler" version = "0.13.4" dependencies = [ "codemap", "indexmap", "lasso", "once_cell", "phf", "rand", "wasm-bindgen", ] [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "include_sass" version = "0.13.4" dependencies = [ "grass_compiler", "quote", "syn", ] [[package]] name = "indexmap" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown 0.14.3", ] [[package]] name = "js-sys" version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] [[package]] name = "lasso" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4644821e1c3d7a560fe13d842d13f587c07348a1a05d3a797152d41c90c56df2" dependencies = [ "hashbrown 0.13.2", ] [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_macros" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", "syn", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "zerocopy" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", "syn", ] grass-0.13.4/Cargo.toml000066400000000000000000000004311465374720000146540ustar00rootroot00000000000000[workspace] members = [ "crates/compiler", "crates/include_sass", "crates/lib", ] [profile.release] debug = 1 panic = "abort" lto = true codegen-units = 1 [profile.small] inherits = 'release' opt-level = 'z' lto = true codegen-units = 1 panic = 'abort' strip = true grass-0.13.4/LICENSE000066400000000000000000000020551465374720000137350ustar00rootroot00000000000000MIT License Copyright (c) 2020 Connor Skees Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. grass-0.13.4/README.md000066400000000000000000000101771465374720000142130ustar00rootroot00000000000000# grass This crate aims to provide a high level interface for compiling [Sass](https://sass-lang.com/documentation/) into plain CSS. It offers a very limited API, currently exposing only 2 functions. In addition to a library, this crate also includes a binary that is intended to act as an invisible replacement to the Sass commandline executable. This crate aims to achieve complete feature parity with the `dart-sass` reference implementation. A deviation from the `dart-sass` implementation can be considered a bug except for in the case of error messages and error spans. [Documentation](https://docs.rs/grass/) [crates.io](https://crates.io/crates/grass) ## Status `grass` has reached a stage where one can be quite confident in its output. For the average user there should not be perceptible differences from `dart-sass`. 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 rough edges of `grass` largely include `@forward` and more complex uses of `@use`. We support basic usage of these rules, but more advanced features such as `@import`ing modules containing `@forward` with prefixes may not behave as expected. All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19). `grass` is not a drop-in replacement for `libsass` and does not intend to be. If you are upgrading to `grass` from `libsass`, you may have to make modifications to your stylesheets, though these changes should not differ from those you would have to make if upgrading to `dart-sass`. ## Performance `grass` is benchmarked against `dart-sass` and `sassc` (`libsass`) [here](https://github.com/connorskees/sass-perf). In general, `grass` appears to be ~2x faster than `dart-sass` and ~1.7x faster than `sassc`. ## Cargo Features ### commandline (enabled by default): build a binary using clap ### random (enabled by default): enable the builtin functions [`random([$limit])`](https://sass-lang.com/documentation/modules/math/#random) and [`unique-id()`](https://sass-lang.com/documentation/modules/string/#unique-id) ### macro (disabled by default): enable the macro `grass::include!` for compiling Sass to CSS at compile time ### nightly (disabled by default): currently only used by `grass::include!` to enable [proc_macro::tracked_path](https://github.com/rust-lang/rust/issues/99515) ## Testing As much as possible this library attempts to follow the same [philosophy for testing as `rust-analyzer`](https://internals.rust-lang.org/t/experience-report-contributing-to-rust-lang-rust/12012/17). Namely, all one should have to do is run `cargo test` to run all its tests. This library maintains a test suite distinct from the `sass-spec`, though it does include some spec tests verbatim. This has the benefit of allowing tests to be run without ruby as well as allowing the tests more granular than they are in the official spec. Having said that, to run the official test suite, ```bash # This script expects node >=v14.14.0. Check version with `node --version` git clone https://github.com/connorskees/grass --recursive cd grass && cargo b --release cd sass-spec && npm install npm run sass-spec -- --impl=dart-sass --command '../target/release/grass' ``` The spec runner does not work on Windows. Using a modified version of the spec runner that ignores warnings and error spans (but does include error messages), `grass` achieves the following results: ``` 2023-07-09 PASSING: 6230 FAILING: 545 TOTAL: 6905 ``` The majority of the failing tests are purely aesthetic, relating to whitespace around comments in expanded mode or error messages. ## Versioning The minimum supported rust version (MSRV) of `grass` is `1.70.0`. An increase to the MSRV will correspond with a minor version bump. The current MSRV is not a hard minimum, but future bugfix versions of `grass` are not guaranteed to work on versions prior to this. `grass` currently targets `dart-sass` version `1.54.3`. An increase to this number will correspond to either a minor or bugfix version bump, depending on the changes. grass-0.13.4/crates/000077500000000000000000000000001465374720000142075ustar00rootroot00000000000000grass-0.13.4/crates/compiler/000077500000000000000000000000001465374720000160215ustar00rootroot00000000000000grass-0.13.4/crates/compiler/Cargo.toml000066400000000000000000000030141465374720000177470ustar00rootroot00000000000000[package] name = "grass_compiler" version = "0.13.4" edition = "2021" description = "Internal implementation of the grass compiler" readme = "README.md" license = "MIT" categories = ["web-programming"] keywords = ["scss", "sass", "css", "web"] repository = "https://github.com/connorskees/grass" authors = ["Connor Skees <39542938+ConnorSkees@users.noreply.github.com>"] rust-version = "1.70" [lib] name = "grass_compiler" path = "src/lib.rs" # crate-type = ["cdylib", "rlib"] bench = false [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg doc_cfg" cargo +nightly doc --no-deps --open rustdoc-args = ["--cfg", "doc_cfg"] [dependencies] # todo: replace with std::cell::LazyCell (msrv 1.80.0) once_cell = "1.15.0" # todo: use xorshift for random numbers rand = { version = "0.8", optional = true } # todo: update to use asref # todo: update to expose more info (for eww) # todo: update to use text_size::TextRange codemap = "0.1.3" wasm-bindgen = { version = "0.2.68", optional = true } # todo: benchmark using phf for global functions phf = { version = "0.11", features = ["macros"] } indexmap = "2" # todo: do we really need interning for things? lasso = "0.7" [features] default = ["random", "custom-builtin-fns"] # Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()` random = ["rand"] # Option: expose JavaScript-friendly WebAssembly exports wasm-exports = ["wasm-bindgen"] # Option: expose internals necessary to implement custom builtin functions custom-builtin-fns = [] grass-0.13.4/crates/compiler/README.md000066400000000000000000000005571465374720000173070ustar00rootroot00000000000000# grass_compiler This crate exposes the internals of the main package, [`grass`](https://crates.io/crates/grass). For most users, the preferred crate should be `grass`, as it is more stable and has a simpler API. This crate will see frequent breaking changes. [Documentation](https://docs.rs/grass_compiler/) [crates.io](https://crates.io/crates/grass_compiler) grass-0.13.4/crates/compiler/input.scss000066400000000000000000000000251465374720000200520ustar00rootroot00000000000000a { color: red; }grass-0.13.4/crates/compiler/src/000077500000000000000000000000001465374720000166105ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/ast/000077500000000000000000000000001465374720000173775ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/ast/args.rs000066400000000000000000000217011465374720000207020ustar00rootroot00000000000000use 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 struct Argument { pub name: Identifier, pub default: Option, } #[derive(Debug, Clone)] pub struct ArgumentDeclaration { pub args: Vec, pub rest: Option, } impl ArgumentDeclaration { pub fn empty() -> Self { Self { args: Vec::new(), rest: None, } } pub fn verify( &self, num_positional: usize, names: &BTreeMap, span: Span, ) -> SassResult<()> { let mut named_used = 0; for i in 0..self.args.len() { let argument = &self.args[i]; if i < num_positional { if names.contains_key(&argument.name) { // todo: _originalArgumentName return Err(( format!( "Argument ${} was passed both by position and by name.", argument.name ), span, ) .into()); } } else if names.contains_key(&argument.name) { named_used += 1; } else if argument.default.is_none() { // todo: _originalArgumentName return Err((format!("Missing argument ${}.", argument.name), span).into()); } } if self.rest.is_some() { return Ok(()); } if num_positional > self.args.len() { return Err(( format!( "Only {} {}{} allowed, but {num_positional} {} passed.", self.args.len(), if names.is_empty() { "" } else { "positional " }, if self.args.len() == 1 { "argument" } else { "arguments" }, if num_positional == 1 { "was" } else { "were" }, num_positional = num_positional, ), span, ) .into()); } if named_used < names.len() { let mut unknown_names = names.keys().copied().collect::>(); for arg in &self.args { unknown_names.remove(&arg.name); } if unknown_names.len() == 1 { return Err(( format!( "No argument named ${}.", unknown_names.iter().next().unwrap() ), span, ) .into()); } if unknown_names.len() > 1 { return Err(( format!( "No arguments named {}.", to_sentence( unknown_names .into_iter() .map(|name| format!("${name}", name = name)) .collect(), "or" ) ), span, ) .into()); } } Ok(()) } } #[derive(Debug, Clone)] pub struct ArgumentInvocation { pub(crate) positional: Vec, pub(crate) named: BTreeMap, pub(crate) rest: Option, pub(crate) keyword_rest: Option, pub(crate) 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), } /// Function arguments that have been evaluated /// /// Arguments may be passed either positionally or by name. Positional arguments /// may not come after named ones. #[derive(Debug, Clone)] pub struct ArgumentResult { pub(crate) positional: Vec, pub(crate) named: BTreeMap, pub(crate) separator: ListSeparator, pub(crate) span: Span, // todo: hack pub(crate) touched: BTreeSet, } impl ArgumentResult { /// Get argument by name /// /// Removes the argument pub fn get_named>(&mut self, val: T) -> Option> { self.named.remove(&val.into()).map(|n| Spanned { node: n, span: self.span, }) } /// Get a positional argument by 0-indexed position /// /// Replaces argument with [`Value::Null`] gravestone pub fn get_positional(&mut self, idx: usize) -> Option> { let val = match self.positional.get_mut(idx) { Some(v) => Some(Spanned { node: mem::replace(v, Value::Null), span: self.span, }), None => None, }; self.touched.insert(idx); val } /// Get an argument by either name or position /// /// If the named argument does not exist, then the position is checked. Like /// [`ArgumentResult::get_named`] and [`ArgumentResult::get_positional`], this /// function removes the argument or replaces it with a gravestone pub fn get>(&mut self, position: usize, name: T) -> Option> { match self.get_named(name) { Some(v) => Some(v), None => self.get_positional(position), } } /// Like [`ArgumentResult::get`], but returns a result if the argument doesn't exist pub fn get_err(&mut self, position: usize, name: &str) -> SassResult { match self.get_named(name) { Some(v) => Ok(v.node), None => match self.get_positional(position) { Some(v) => Ok(v.node), None => Err((format!("Missing argument ${}.", name), self.span()).into()), }, } } pub const fn span(&self) -> Span { self.span } pub(crate) fn len(&self) -> usize { self.positional.len() + self.named.len() } /// Assert that this function has at least `min` number of args pub(crate) fn min_args(&self, min: usize) -> SassResult<()> { let len = self.len(); if len < min { let phrase = match min { 1 => "one argument", 2 => "two arguments", 3 => "three arguments", _ => todo!("min args greater than three"), }; return Err((format!("At least {phrase} must be passed."), self.span()).into()); } Ok(()) } /// Assert that this function has at most `max` number of args pub fn max_args(&self, max: usize) -> SassResult<()> { let len = self.len(); if len > max { let mut err = String::with_capacity(50); #[allow(unknown_lints, clippy::format_push_string)] err.push_str(&format!("Only {max} argument", max = 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(()) } /// Get an argument by name or position. If the argument does not exist, use /// the default value provided 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(crate) fn remove_positional(&mut self, position: usize) -> Option { if self.positional.len() > position { Some(self.positional.remove(position)) } else { None } } pub(crate) fn get_variadic(self) -> SassResult>> { if let Some((name, _)) = self.named.iter().next() { return Err((format!("No argument named ${}.", name), self.span).into()); } let Self { positional, span, touched, .. } = self; // todo: complete hack, we shouldn't have the `touched` set let args = positional .into_iter() .enumerate() .filter(|(idx, _)| !touched.contains(idx)) .map(|(_, node)| Spanned { node, span }) .collect(); Ok(args) } } grass-0.13.4/crates/compiler/src/ast/css.rs000066400000000000000000000100571465374720000205400ustar00rootroot00000000000000use codemap::Span; use crate::selector::ExtendedSelector; use super::{MediaRule, Style, UnknownAtRule}; #[derive(Debug, Clone)] pub(crate) enum CssStmt { RuleSet { selector: ExtendedSelector, body: Vec, is_group_end: bool, }, Style(Style), Media(MediaRule, bool), UnknownAtRule(UnknownAtRule, bool), Supports(SupportsRule, bool), Comment(String, Span), KeyframesRuleSet(KeyframesRuleSet), /// A plain import such as `@import "foo.css";` or /// `@import url(https://fonts.google.com/foo?bar);` // todo: named fields, 0: url, 1: modifiers Import(String, Option), } impl CssStmt { pub fn is_style_rule(&self) -> bool { matches!(self, CssStmt::RuleSet { .. }) } pub fn set_group_end(&mut self) { match self { CssStmt::Media(_, is_group_end) | CssStmt::UnknownAtRule(_, is_group_end) | CssStmt::Supports(_, is_group_end) | CssStmt::RuleSet { is_group_end, .. } => *is_group_end = true, CssStmt::Style(_) | CssStmt::Comment(_, _) | CssStmt::KeyframesRuleSet(_) | CssStmt::Import(_, _) => {} } } pub fn is_group_end(&self) -> bool { match self { CssStmt::Media(_, is_group_end) | CssStmt::UnknownAtRule(_, is_group_end) | CssStmt::Supports(_, is_group_end) | CssStmt::RuleSet { is_group_end, .. } => *is_group_end, _ => false, } } pub fn is_invisible(&self) -> bool { match self { CssStmt::RuleSet { selector, body, .. } => { selector.is_invisible() || body.iter().all(CssStmt::is_invisible) } CssStmt::Style(style) => style.value.node.is_blank(), CssStmt::Media(media_rule, ..) => media_rule.body.iter().all(CssStmt::is_invisible), CssStmt::UnknownAtRule(..) | CssStmt::Import(..) | CssStmt::Comment(..) => false, CssStmt::Supports(supports_rule, ..) => { supports_rule.body.iter().all(CssStmt::is_invisible) } CssStmt::KeyframesRuleSet(kf) => kf.body.iter().all(CssStmt::is_invisible), } } pub fn copy_without_children(&self) -> Self { match self { CssStmt::RuleSet { selector, is_group_end, .. } => CssStmt::RuleSet { selector: selector.clone(), body: Vec::new(), is_group_end: *is_group_end, }, CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..) => unreachable!(), CssStmt::Media(media, is_group_end) => CssStmt::Media( MediaRule { query: media.query.clone(), body: Vec::new(), }, *is_group_end, ), CssStmt::UnknownAtRule(at_rule, is_group_end) => CssStmt::UnknownAtRule( UnknownAtRule { name: at_rule.name.clone(), params: at_rule.params.clone(), body: Vec::new(), has_body: at_rule.has_body, }, *is_group_end, ), CssStmt::Supports(supports, is_group_end) => CssStmt::Supports( SupportsRule { params: supports.params.clone(), body: Vec::new(), }, *is_group_end, ), CssStmt::KeyframesRuleSet(keyframes) => CssStmt::KeyframesRuleSet(KeyframesRuleSet { selector: keyframes.selector.clone(), body: Vec::new(), }), } } } #[derive(Debug, Clone)] pub(crate) struct KeyframesRuleSet { pub selector: Vec, pub body: Vec, } #[derive(Debug, Clone)] pub(crate) enum KeyframesSelector { To, From, Percent(Box), } #[derive(Debug, Clone)] pub(crate) struct SupportsRule { pub params: String, pub body: Vec, } grass-0.13.4/crates/compiler/src/ast/expr.rs000066400000000000000000000115071465374720000207270ustar00rootroot00000000000000use std::{iter::Iterator, sync::Arc}; 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 struct Ternary(pub ArgumentInvocation); #[derive(Debug, Clone)] pub struct ListExpr { pub elems: Vec>, pub separator: ListSeparator, pub brackets: Brackets, } #[derive(Debug, Clone)] pub struct FunctionCallExpr { pub namespace: Option>, pub name: Identifier, pub arguments: Arc, pub span: Span, } #[derive(Debug, Clone)] pub struct InterpolatedFunction { pub name: Interpolation, pub arguments: ArgumentInvocation, pub span: Span, } #[derive(Debug, Clone, Default)] pub struct AstSassMap(pub Vec<(Spanned, AstExpr)>); #[derive(Debug, Clone)] pub struct BinaryOpExpr { pub lhs: AstExpr, pub op: BinaryOp, pub rhs: AstExpr, pub allows_slash: bool, pub span: Span, } #[derive(Debug, Clone)] pub enum AstExpr { BinaryOp(Arc), True, False, Calculation { name: CalculationName, args: Vec, }, Color(Arc), FunctionCall(FunctionCallExpr), If(Arc), InterpolatedFunction(Arc), List(ListExpr), Map(AstSassMap), Null, Number { n: Number, unit: Unit, }, Paren(Arc), ParentSelector, String(StringExpr, Span), Supports(Arc), UnaryOp(UnaryOp, Arc, Span), Variable { name: Spanned, namespace: Option>, }, } // todo: make quotes bool // todo: track span inside #[derive(Debug, Clone)] pub struct StringExpr(pub Interpolation, pub QuoteKind); impl StringExpr { fn quote_inner_text( text: &str, quote: char, buffer: &mut Interpolation, // default=false is_static: bool, ) { let mut chars = text.chars().peekable(); while let Some(char) = chars.next() { if char == '\n' || char == '\r' { buffer.add_char('\\'); buffer.add_char('a'); if let Some(next) = chars.peek() { if next.is_ascii_whitespace() || next.is_ascii_hexdigit() { buffer.add_char(' '); } } } else { if char == quote || char == '\\' || (is_static && char == '#' && chars.peek() == Some(&'{')) { buffer.add_char('\\'); } buffer.add_char(char); } } } fn best_quote<'a>(strings: impl Iterator) -> char { let mut contains_double_quote = false; for s in strings { for c in s.chars() { if c == '\'' { return '"'; } if c == '"' { contains_double_quote = true; } } } if contains_double_quote { '\'' } else { '"' } } pub fn as_interpolation(self, is_static: bool) -> Interpolation { if self.1 == QuoteKind::None { return self.0; } let quote = Self::best_quote(self.0.contents.iter().filter_map(|c| match c { InterpolationPart::Expr(..) => None, InterpolationPart::String(text) => Some(text.as_str()), })); let mut buffer = Interpolation::new(); buffer.add_char(quote); for value in self.0.contents { match value { InterpolationPart::Expr(e) => buffer.add_expr(e), InterpolationPart::String(text) => { Self::quote_inner_text(&text, quote, &mut buffer, is_static); } } } buffer.add_char(quote); buffer } } impl AstExpr { pub fn is_variable(&self) -> bool { matches!(self, Self::Variable { .. }) } pub fn is_slash_operand(&self) -> bool { match self { Self::Number { .. } | Self::Calculation { .. } => true, Self::BinaryOp(binop) => binop.allows_slash, _ => false, } } pub fn slash(left: Self, right: Self, span: Span) -> Self { Self::BinaryOp(Arc::new(BinaryOpExpr { lhs: left, op: BinaryOp::Div, rhs: right, allows_slash: true, span, })) } pub const fn span(self, span: Span) -> Spanned { Spanned { node: self, span } } } grass-0.13.4/crates/compiler/src/ast/interpolation.rs000066400000000000000000000042351465374720000226400ustar00rootroot00000000000000use codemap::Spanned; use super::AstExpr; #[derive(Debug, Clone)] pub struct Interpolation { pub contents: Vec, } impl Interpolation { pub fn new() -> Self { Self { contents: Vec::new(), } } pub fn is_empty(&self) -> bool { self.contents.is_empty() } pub fn new_with_expr(e: Spanned) -> Self { Self { contents: vec![InterpolationPart::Expr(e)], } } pub fn new_plain(s: String) -> Self { Self { contents: vec![InterpolationPart::String(s)], } } pub fn add_expr(&mut self, expr: Spanned) { self.contents.push(InterpolationPart::Expr(expr)); } pub fn add_string(&mut self, s: String) { match self.contents.last_mut() { Some(InterpolationPart::String(existing)) => existing.push_str(&s), _ => self.contents.push(InterpolationPart::String(s)), } } 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 enum InterpolationPart { String(String), Expr(Spanned), } grass-0.13.4/crates/compiler/src/ast/media.rs000066400000000000000000000176221465374720000210340ustar00rootroot00000000000000use std::fmt::{self, Write}; use codemap::Span; use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser}; #[derive(Debug, Clone)] pub(crate) struct MediaRule { pub query: Vec, pub body: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MediaQuery { pub modifier: Option, pub media_type: Option, pub conditions: Vec, pub conjunction: bool, } impl MediaQuery { pub fn matches_all_types(&self) -> bool { self.media_type.is_none() || self .media_type .as_ref() .map_or(false, |v| v.to_ascii_lowercase() == "all") } pub fn condition( conditions: Vec, // default=true conjunction: bool, ) -> Self { Self { modifier: None, media_type: None, conditions, conjunction, } } pub fn media_type( media_type: Option, modifier: Option, conditions: Option>, ) -> Self { Self { modifier, conjunction: true, media_type, conditions: conditions.unwrap_or_default(), } } pub fn parse_list(list: &str, span: Span) -> SassResult> { let toks = Lexer::new_from_string(list, span); MediaQueryParser::new(toks).parse() } #[allow(clippy::if_not_else)] pub(crate) fn merge(&self, other: &Self) -> MediaQueryMergeResult { if !self.conjunction || !other.conjunction { return MediaQueryMergeResult::Unrepresentable; } let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase()); let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let other_type = other.media_type.as_ref().map(|m| m.to_ascii_lowercase()); if this_type.is_none() && other_type.is_none() { return MediaQueryMergeResult::Success(Self::condition( self.conditions .iter() .chain(&other.conditions) .cloned() .collect(), true, )); } let modifier; let media_type; let conditions; if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) { if this_modifier == other_modifier { let negative_conditions = if this_modifier.as_deref() == Some("not") { &self.conditions } else { &other.conditions }; let positive_conditions = if this_modifier.as_deref() == Some("not") { &other.conditions } else { &self.conditions }; // If the negative conditions are a subset of the positive conditions, the // query is empty. For example, `not screen and (color)` has no // intersection with `screen and (color) and (grid)`. // // However, `not screen and (color)` *does* intersect with `screen and // (grid)`, because it means `not (screen and (color))` and so it allows // a screen with no color but with a grid. if negative_conditions .iter() .all(|feat| positive_conditions.contains(feat)) { return MediaQueryMergeResult::Empty; } return MediaQueryMergeResult::Unrepresentable; } else if self.matches_all_types() || other.matches_all_types() { return MediaQueryMergeResult::Unrepresentable; } if this_modifier.as_deref() == Some("not") { modifier = &other_modifier; media_type = &other_type; conditions = other.conditions.clone(); } else { modifier = &this_modifier; media_type = &this_type; conditions = self.conditions.clone(); } } else if this_modifier.as_deref() == Some("not") { debug_assert_eq!(other_modifier.as_deref(), Some("not")); // CSS has no way of representing "neither screen nor print". if this_type != other_type { return MediaQueryMergeResult::Unrepresentable; } let more_conditions = if self.conditions.len() > other.conditions.len() { &self.conditions } else { &other.conditions }; let fewer_conditions = if self.conditions.len() > other.conditions.len() { &other.conditions } else { &self.conditions }; // If one set of conditions is a superset of the other, use those conditions // because they're strictly narrower. if fewer_conditions .iter() .all(|feat| more_conditions.contains(feat)) { modifier = &this_modifier; media_type = &this_type; conditions = more_conditions.clone(); } else { // Otherwise, there's no way to represent the intersection. return MediaQueryMergeResult::Unrepresentable; } } else if self.matches_all_types() { modifier = &other_modifier; // Omit the type if either input query did, since that indicates that they // aren't targeting a browser that requires "all and". media_type = if other.matches_all_types() && this_type.is_none() { &None } else { &other_type }; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } else if other.matches_all_types() { modifier = &this_modifier; media_type = &this_type; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } else if this_type != other_type { return MediaQueryMergeResult::Empty; } else { if this_modifier.is_some() { modifier = &this_modifier; } else { modifier = &other_modifier; } media_type = &this_type; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } MediaQueryMergeResult::Success(MediaQuery { media_type: if media_type == &this_type { self.media_type.clone() } else { other.media_type.clone() }, modifier: if modifier == &this_modifier { self.modifier.clone() } else { other.modifier.clone() }, conditions, conjunction: true, }) } } impl fmt::Display for MediaQuery { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(modifier) = &self.modifier { f.write_str(modifier)?; f.write_char(' ')?; } if let Some(media_type) = &self.media_type { f.write_str(media_type)?; if !&self.conditions.is_empty() { f.write_str(" and ")?; } } f.write_str(&self.conditions.join(" and ")) } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum MediaQueryMergeResult { Empty, Unrepresentable, Success(MediaQuery), } grass-0.13.4/crates/compiler/src/ast/mixin.rs000066400000000000000000000015441465374720000210750ustar00rootroot00000000000000use 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(), } } } grass-0.13.4/crates/compiler/src/ast/mod.rs000066400000000000000000000005221465374720000205230ustar00rootroot00000000000000pub use args::*; pub(crate) use css::*; pub use expr::*; pub use interpolation::*; pub(crate) use media::*; pub(crate) use mixin::*; pub use stmt::*; pub(crate) use style::*; pub(crate) use unknown::*; pub use args::ArgumentResult; mod args; mod css; mod expr; mod interpolation; mod media; mod mixin; mod stmt; mod style; mod unknown; grass-0.13.4/crates/compiler/src/ast/stmt.rs000066400000000000000000000330161465374720000207370ustar00rootroot00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, path::PathBuf, rc::Rc, 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 struct AstSilentComment { pub text: String, pub span: Span, } #[derive(Debug, Clone)] pub struct AstPlainCssImport { pub url: Interpolation, pub modifiers: Option, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AstSassImport { pub url: String, pub span: Span, } #[derive(Debug, Clone)] pub struct AstIf { pub if_clauses: Vec, pub else_clause: Option>, } #[derive(Debug, Clone)] pub struct AstIfClause { pub condition: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstFor { pub variable: Spanned, pub from: Spanned, pub to: Spanned, pub is_exclusive: bool, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstReturn { pub val: AstExpr, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AstRuleSet { pub selector: Interpolation, pub body: Vec, pub selector_span: Span, pub span: Span, } #[derive(Debug, Clone)] pub struct AstStyle { pub name: Interpolation, pub value: Option>, pub body: Vec, pub span: Span, } impl AstStyle { pub fn is_custom_property(&self) -> bool { self.name.initial_plain().starts_with("--") } } #[derive(Debug, Clone)] pub struct AstEach { pub variables: Vec, pub list: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstMedia { pub query: Interpolation, pub query_span: Span, pub body: Vec, pub span: Span, } pub type CssMediaQuery = MediaQuery; #[derive(Debug, Clone)] pub struct AstWhile { pub condition: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstVariableDecl { pub namespace: Option>, pub name: Identifier, pub value: AstExpr, pub is_guarded: bool, pub is_global: bool, pub span: Span, } #[derive(Debug, Clone)] pub struct AstFunctionDecl { pub name: Spanned, pub arguments: ArgumentDeclaration, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstDebugRule { pub value: AstExpr, pub span: Span, } #[derive(Debug, Clone)] pub struct AstWarn { pub value: AstExpr, pub span: Span, } #[derive(Debug, Clone)] pub 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 struct AstLoudComment { pub text: Interpolation, pub span: Span, } #[derive(Debug, Clone)] pub struct AstMixin { pub name: Identifier, pub args: ArgumentDeclaration, pub body: Vec, /// Whether the mixin contains a `@content` rule. pub has_content: bool, } #[derive(Debug, Clone)] pub struct AstContentRule { pub args: ArgumentInvocation, } #[derive(Debug, Clone)] pub struct AstContentBlock { pub args: ArgumentDeclaration, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstInclude { pub namespace: Option>, pub name: Spanned, pub args: ArgumentInvocation, pub content: Option, pub span: Span, } #[derive(Debug, Clone)] pub struct AstUnknownAtRule { pub name: Interpolation, pub value: Option, pub body: Option>, pub span: Span, } #[derive(Debug, Clone)] pub struct AstExtendRule { pub value: Interpolation, pub is_optional: bool, pub span: Span, } #[derive(Debug, Clone)] pub struct AstAtRootRule { pub body: Vec, pub query: Option>, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AtRootQuery { pub include: bool, pub names: HashSet, pub all: bool, pub rule: bool, } impl AtRootQuery { pub fn new(include: bool, names: HashSet) -> Self { let all = names.contains("all"); let rule = names.contains("rule"); Self { include, names, all, rule, } } pub fn excludes_name(&self, name: &str) -> bool { (self.all || self.names.contains(name)) != self.include } pub fn excludes_style_rules(&self) -> bool { (self.all || self.rule) != self.include } pub(crate) 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 struct AstImportRule { pub imports: Vec, } #[derive(Debug, Clone)] pub enum AstImport { Plain(AstPlainCssImport), Sass(AstSassImport), } impl AstImport { pub fn is_dynamic(&self) -> bool { matches!(self, AstImport::Sass(..)) } } #[derive(Debug, Clone)] pub struct AstUseRule { pub url: PathBuf, pub namespace: Option, pub configuration: Vec, pub span: Span, } #[derive(Debug, Clone)] pub struct ConfiguredVariable { pub name: Spanned, pub expr: Spanned, pub is_guarded: bool, } #[derive(Debug, Clone)] pub struct Configuration { pub(crate) values: Arc>, #[allow(unused)] pub(crate) original_config: Option>>, pub(crate) span: Option, } impl Configuration { pub fn through_forward( config: Rc>, forward: &AstForwardRule, ) -> Rc> { if (*config).borrow().is_empty() { return Rc::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)); } Rc::new(RefCell::new(Self::with_values( config, Arc::clone(&new_values), ))) } fn with_values( config: Rc>, values: Arc>, ) -> Self { Self { values, original_config: Some(config), span: None, } } pub fn first(&self) -> Option> { let name = *self.values.keys().first()?; let value = self.values.get(name)?; Some(Spanned { node: name, span: value.configuration_span?, }) } pub fn remove(&mut self, name: Identifier) -> Option { self.values.remove(name) } pub fn is_implicit(&self) -> bool { self.span.is_none() } pub fn implicit(values: BTreeMap) -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))), original_config: None, span: None, } } pub fn explicit(values: BTreeMap, span: Span) -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))), original_config: None, span: Some(span), } } pub fn empty() -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), original_config: None, span: None, } } pub fn is_empty(&self) -> bool { self.values.is_empty() } #[allow(unused)] pub fn original_config(config: Rc>) -> Rc> { match (*config).borrow().original_config.as_ref() { Some(v) => Rc::clone(v), None => Rc::clone(&config), } } } #[derive(Debug, Clone)] pub struct ConfiguredValue { pub value: Value, pub configuration_span: Option, } impl ConfiguredValue { pub fn explicit(value: Value, configuration_span: Span) -> Self { Self { value, configuration_span: Some(configuration_span), } } pub fn implicit(value: Value) -> Self { Self { value, configuration_span: None, } } } #[derive(Debug, Clone)] pub struct AstForwardRule { pub url: PathBuf, pub shown_mixins_and_functions: Option>, pub shown_variables: Option>, pub hidden_mixins_and_functions: Option>, pub hidden_variables: Option>, pub prefix: Option, pub configuration: Vec, pub span: Span, } impl AstForwardRule { pub fn new( url: PathBuf, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: None, shown_variables: None, hidden_mixins_and_functions: None, hidden_variables: None, prefix, configuration: configuration.unwrap_or_default(), span, } } pub fn show( url: PathBuf, shown_mixins_and_functions: HashSet, shown_variables: HashSet, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: Some(shown_mixins_and_functions), shown_variables: Some(shown_variables), hidden_mixins_and_functions: None, hidden_variables: None, prefix, configuration: configuration.unwrap_or_default(), span, } } pub fn hide( url: PathBuf, hidden_mixins_and_functions: HashSet, hidden_variables: HashSet, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: None, shown_variables: None, hidden_mixins_and_functions: Some(hidden_mixins_and_functions), hidden_variables: Some(hidden_variables), prefix, configuration: configuration.unwrap_or_default(), span, } } } #[derive(Debug, Clone)] pub enum AstSupportsCondition { Anything { contents: Interpolation, }, Declaration { name: AstExpr, value: AstExpr, }, Function { name: Interpolation, args: Interpolation, }, Interpolation(AstExpr), Negation(Box), Operation { left: Box, operator: Option, right: Box, }, } #[derive(Debug, Clone)] pub struct AstSupportsRule { pub condition: AstSupportsCondition, pub body: Vec, pub span: Span, } #[derive(Debug, Clone)] pub 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 struct StyleSheet { pub body: Vec, pub url: PathBuf, pub is_plain_css: bool, /// Array of indices into `body` pub uses: Vec, /// Array of indices into `body` pub forwards: Vec, } impl StyleSheet { pub fn new(is_plain_css: bool, url: PathBuf) -> Self { Self { body: Vec::new(), url, is_plain_css, uses: Vec::new(), forwards: Vec::new(), } } } grass-0.13.4/crates/compiler/src/ast/style.rs000066400000000000000000000004141465374720000211040ustar00rootroot00000000000000use codemap::Spanned; use crate::{interner::InternedString, value::Value}; /// A style: `color: red` #[derive(Clone, Debug)] pub(crate) struct Style { pub property: InternedString, pub value: Box>, pub declared_as_custom_property: bool, } grass-0.13.4/crates/compiler/src/ast/unknown.rs000066400000000000000000000005461465374720000214510ustar00rootroot00000000000000use crate::ast::CssStmt; #[derive(Debug, Clone)] #[allow(dead_code)] pub(crate) struct UnknownAtRule { pub name: String, // pub super_selector: Selector, pub params: String, pub body: Vec, /// Whether or not this @-rule was declared with curly /// braces. A body may not necessarily have contents pub has_body: bool, } grass-0.13.4/crates/compiler/src/builtin/000077500000000000000000000000001465374720000202565ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/builtin/functions/000077500000000000000000000000001465374720000222665ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/builtin/functions/color/000077500000000000000000000000001465374720000234045ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/builtin/functions/color/hsl.rs000066400000000000000000000241641465374720000245470ustar00rootroot00000000000000use std::collections::{BTreeMap, BTreeSet}; use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber}; use super::{ angle_value, rgb::{function_string, parse_channels, percentage_or_unitless}, ParsedChannels, }; fn hsl_3_args( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { let span = args.span(); let hue = args.get_err(0, "hue")?; let saturation = args.get_err(1, "saturation")?; let lightness = args.get_err(2, "lightness")?; let alpha = args.default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0))); if [&hue, &saturation, &lightness, &alpha] .iter() .copied() .any(Value::is_special_function) { return Ok(Value::String( format!( "{}({})", name, Value::List( if args.len() == 4 { vec![hue, saturation, lightness, alpha] } else { vec![hue, saturation, lightness] }, ListSeparator::Comma, Brackets::None ) .to_css_string(args.span(), false)? ), QuoteKind::None, )); } let hue = angle_value(hue, "hue", span)?; let saturation = saturation.assert_number_with_name("saturation", span)?; let lightness = lightness.assert_number_with_name("lightness", span)?; let alpha = percentage_or_unitless( &alpha.assert_number_with_name("alpha", span)?, 1.0, "alpha", span, visitor, )?; Ok(Value::Color(Arc::new(Color::from_hsla_fn( Number(hue.rem_euclid(360.0)), saturation.num / Number(100.0), lightness.num / Number(100.0), Number(alpha), )))) } fn inner_hsl( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { 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() { Ok(Value::String( function_string(name, &[hue, saturation], visitor, span)?, QuoteKind::None, )) } else { Err(("Missing argument $lightness.", args.span()).into()) } } else { hsl_3_args(name, args, visitor) } } pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_hsl("hsl", args, visitor) } pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_hsl("hsla", args, visitor) } pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.hue(), unit: Unit::Deg, as_slash: None, })) } pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.saturation(), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.lightness(), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let degrees = angle_value(args.get_err(1, "degrees")?, "degrees", args.span())?; Ok(Value::Color(Arc::new(color.adjust_hue(degrees)))) } fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.lighten(amount.num)))) } fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.darken(amount.num)))) } fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; if args.len() == 1 { let amount = args .get_err(0, "amount")? .assert_number_with_name("amount", args.span())?; return Ok(Value::String( format!( "saturate({})", serialize_number(&amount, &Options::default(), args.span())?, ), QuoteKind::None, )); } let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Color(Arc::new(color.saturate(amount.num)))) } fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.desaturate(amount.num)))) } pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, Value::Dimension(SassNumber { num: n, unit: u, as_slash: _, }) => { return Ok(Value::String( format!("grayscale({}{})", n.inspect(), u), QuoteKind::None, )) } v => { return Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::Color(Arc::new(color.desaturate(Number::one())))) } pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Color(Arc::new(color.complement()))) } pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let weight = args .get(1, "weight") .map::, _>(|weight| { let mut weight = weight.node.assert_number_with_name("weight", span)?; weight.assert_bounds("weight", 0.0, 100.0, span)?; weight.num /= Number(100.0); Ok(weight.num) }) .transpose()?; match args.get_err(0, "color")? { Value::Color(c) => Ok(Value::Color(Arc::new( c.invert(weight.unwrap_or_else(Number::one)), ))), Value::Dimension(SassNumber { num: n, unit: u, as_slash: _, }) => { if weight.is_some() { return Err(( "Only one argument may be passed to the plain-CSS invert() function.", args.span(), ) .into()); } Ok(Value::String( format!("invert({}{})", n.inspect(), u), QuoteKind::None, )) } v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("hsl", Builtin::new(hsl)); f.insert("hsla", Builtin::new(hsla)); f.insert("hue", Builtin::new(hue)); f.insert("saturation", Builtin::new(saturation)); f.insert("adjust-hue", Builtin::new(adjust_hue)); f.insert("lightness", Builtin::new(lightness)); f.insert("lighten", Builtin::new(lighten)); f.insert("darken", Builtin::new(darken)); f.insert("saturate", Builtin::new(saturate)); f.insert("desaturate", Builtin::new(desaturate)); f.insert("grayscale", Builtin::new(grayscale)); f.insert("complement", Builtin::new(complement)); f.insert("invert", Builtin::new(invert)); } grass-0.13.4/crates/compiler/src/builtin/functions/color/hwb.rs000066400000000000000000000057331465374720000245420ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; use super::{ angle_value, rgb::{parse_channels, percentage_or_unitless}, ParsedChannels, }; pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.blackness() * 100, unit: Unit::Percent, as_slash: None, })) } pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.whiteness() * 100, unit: Unit::Percent, as_slash: None, })) } fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let hue = angle_value(args.get_err(0, "hue")?, "hue", args.span())?; let whiteness = args .get_err(1, "whiteness")? .assert_number_with_name("whiteness", span)?; whiteness.assert_unit(&Unit::Percent, "whiteness", span)?; whiteness.assert_bounds("whiteness", 0.0, 100.0, args.span())?; let blackness = args .get_err(2, "blackness")? .assert_number_with_name("blackness", span)?; blackness.assert_unit(&Unit::Percent, "blackness", span)?; blackness.assert_bounds("blackness", 0.0, 100.0, args.span())?; let alpha = args .default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0))) .assert_number_with_name("alpha", args.span())?; let alpha = percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?; Ok(Value::Color(Arc::new(Color::from_hwb( hue, whiteness.num, blackness.num, Number(alpha), )))) } pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(4)?; if args.len() == 0 || args.len() == 1 { match parse_channels( "hwb", &["hue", "whiteness", "blackness"], args.get_err(0, "channels")?, visitor, args.span(), )? { ParsedChannels::String(s) => Err(( format!("Expected numeric channels, got \"{}\".", s), args.span(), ) .into()), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, named: BTreeMap::new(), separator: ListSeparator::Comma, span: args.span(), touched: BTreeSet::new(), }; hwb_inner(args, visitor) } } } else if args.len() == 3 || args.len() == 4 { hwb_inner(args, visitor) } else { args.max_args(1)?; unreachable!() } } grass-0.13.4/crates/compiler/src/builtin/functions/color/mod.rs000066400000000000000000000015021465374720000245270ustar00rootroot00000000000000use codemap::Span; use crate::{ builtin::builtin_imports::Unit, error::SassResult, value::{conversion_factor, Number, Value}, }; use super::GlobalFunctionMap; pub mod hsl; pub mod hwb; pub mod opacity; pub mod other; pub mod rgb; #[derive(Debug, Clone)] pub(crate) enum ParsedChannels { String(String), List(Vec), } pub(crate) fn angle_value(num: Value, name: &str, span: Span) -> SassResult { let angle = num.assert_number_with_name(name, span)?; if angle.has_compatible_units(&Unit::Deg) { let factor = conversion_factor(&angle.unit, &Unit::Deg).unwrap(); return Ok(angle.num * Number(factor)); } Ok(angle.num) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { hsl::declare(f); opacity::declare(f); other::declare(f); rgb::declare(f); } grass-0.13.4/crates/compiler/src/builtin/functions/color/opacity.rs000066400000000000000000000073541465374720000254330ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; /// Check if `s` matches the regex `^[a-zA-Z]+\s*=` fn is_ms_filter(s: &str) -> bool { let mut bytes = s.bytes(); if !bytes.next().map_or(false, |c| c.is_ascii_alphabetic()) { return false; } bytes .skip_while(u8::is_ascii_alphabetic) .find(|c| !matches!(c, b' ' | b'\t' | b'\n')) == Some(b'=') } #[cfg(test)] mod test { use super::is_ms_filter; #[test] fn test_is_ms_filter() { assert!(is_ms_filter("a=a")); assert!(is_ms_filter("a=")); assert!(is_ms_filter("a \t\n =a")); assert!(!is_ms_filter("a \t\n a=a")); assert!(!is_ms_filter("aa")); assert!(!is_ms_filter(" aa")); assert!(!is_ms_filter("=a")); assert!(!is_ms_filter("1=a")); } } pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { if args.len() <= 1 { let color = args.get_err(0, "color")?; if let Value::String(s, QuoteKind::None) = &color { if is_ms_filter(s) { return Ok(Value::String(format!("alpha({})", s), QuoteKind::None)); } } let color = color.assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.alpha()))) } else { let err = args.max_args(1); let args = args .get_variadic()? .into_iter() .map(|arg| match arg.node { Value::String(s, QuoteKind::None) if is_ms_filter(&s) => Ok(s), _ => { err.clone()?; unreachable!() } }) .collect::>>()?; Ok(Value::String( format!("alpha({})", args.join(", "),), QuoteKind::None, )) } } pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { Value::Color(c) => Ok(Value::Dimension(SassNumber::new_unitless(c.alpha()))), Value::Dimension(SassNumber { num, unit, as_slash: _, }) => Ok(Value::String( format!("opacity({}{})", num.inspect(), unit), QuoteKind::None, )), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()), } } fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_in(amount.num)))) } fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_out(amount.num)))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("alpha", Builtin::new(alpha)); f.insert("opacity", Builtin::new(opacity)); f.insert("opacify", Builtin::new(opacify)); f.insert("fade-in", Builtin::new(opacify)); f.insert("transparentize", Builtin::new(transparentize)); f.insert("fade-out", Builtin::new(transparentize)); } grass-0.13.4/crates/compiler/src/builtin/functions/color/other.rs000066400000000000000000000176361465374720000251100ustar00rootroot00000000000000use crate::{ builtin::{builtin_imports::*, color::angle_value}, utils::to_sentence, value::fuzzy_round, }; #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum UpdateComponents { Change, Adjust, Scale, } fn update_components( mut args: ArgumentResult, visitor: &mut Visitor, update: UpdateComponents, ) -> SassResult { let span = args.span(); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; // todo: what if color is also passed by name if args.positional.len() > 1 { return Err(( "Only one positional argument is allowed. All other arguments must be passed by name.", span, ) .into()); } let check_num = |num: Spanned, name: &str, mut max: f64, assert_percent: bool, check_percent: bool| -> SassResult { let span = num.span; let mut num = num.node.assert_number_with_name(name, span)?; if update == UpdateComponents::Scale { max = 100.0; } if assert_percent || update == UpdateComponents::Scale { num.assert_unit(&Unit::Percent, name, span)?; num.assert_bounds( name, if update == UpdateComponents::Change { 0.0 } else { -max }, max, span, )?; } else { num.assert_bounds_with_unit( name, if update == UpdateComponents::Change { 0.0 } else { -max }, max, if check_percent { &Unit::Percent } else { &Unit::None }, span, )?; } // todo: hack to check if rgb channel if max == 100.0 { num.num /= Number(100.0); } Ok(num.num) }; let get_arg = |args: &mut ArgumentResult, name: &str, max: f64, assert_percent: bool, check_percent: bool| -> SassResult> { Ok(match args.get(usize::MAX, name) { Some(v) => Some(check_num(v, name, max, assert_percent, check_percent)?), None => None, }) }; let red = get_arg(&mut args, "red", 255.0, false, false)?; let green = get_arg(&mut args, "green", 255.0, false, false)?; let blue = get_arg(&mut args, "blue", 255.0, false, false)?; let alpha = get_arg(&mut args, "alpha", 1.0, false, false)?; let hue = if update == UpdateComponents::Scale { None } else { args.get(usize::MAX, "hue") .map(|v| angle_value(v.node, "hue", v.span)) .transpose()? }; let saturation = get_arg(&mut args, "saturation", 100.0, false, true)?; let lightness = get_arg(&mut args, "lightness", 100.0, false, true)?; let whiteness = get_arg(&mut args, "whiteness", 100.0, true, true)?; let blackness = get_arg(&mut args, "blackness", 100.0, true, true)?; if !args.named.is_empty() { let argument_word = if args.named.len() == 1 { "argument" } else { "arguments" }; let argument_names = to_sentence( args.named .keys() .map(|key| format!("${key}", key = key)) .collect(), "or", ); return Err(( format!( "No {argument_word} named {argument_names}.", argument_word = argument_word, argument_names = argument_names ), span, ) .into()); } let has_rgb = red.is_some() || green.is_some() || blue.is_some(); let has_sl = saturation.is_some() || lightness.is_some(); let has_wb = whiteness.is_some() || blackness.is_some(); if has_rgb && (has_sl || has_wb || hue.is_some()) { let param_type = if has_wb { "HWB" } else { "HSL" }; return Err(( format!( "RGB parameters may not be passed along with {} parameters.", param_type ), span, ) .into()); } if has_sl && has_wb { return Err(( "HSL parameters may not be passed along with HWB parameters.", span, ) .into()); } fn update_value( current: Number, param: Option, max: f64, update: UpdateComponents, ) -> Number { let param = match param { Some(p) => p, None => return current, }; match update { UpdateComponents::Change => param, UpdateComponents::Adjust => (param + current).clamp(0.0, max), UpdateComponents::Scale => { current + if param > Number(0.0) { Number(max) - current } else { current } * param } } } fn update_rgb(current: Number, param: Option, update: UpdateComponents) -> Number { Number(fuzzy_round(update_value(current, param, 255.0, update).0)) } let color = if has_rgb { Arc::new(Color::from_rgba( update_rgb(color.red(), red, update), update_rgb(color.green(), green, update), update_rgb(color.blue(), blue, update), update_value(color.alpha(), alpha, 1.0, update), )) } else if has_wb { Arc::new(Color::from_hwb( if update == UpdateComponents::Change { hue.unwrap_or_else(|| color.hue()) } else { color.hue() + hue.unwrap_or_else(Number::zero) }, update_value(color.whiteness(), whiteness, 1.0, update) * Number(100.0), update_value(color.blackness(), blackness, 1.0, update) * Number(100.0), update_value(color.alpha(), alpha, 1.0, update), )) } else if hue.is_some() || has_sl { let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); Arc::new(Color::from_hsla( if update == UpdateComponents::Change { hue.unwrap_or(this_hue) } else { this_hue + hue.unwrap_or_else(Number::zero) }, update_value(this_saturation, saturation, 1.0, update), update_value(this_lightness, lightness, 1.0, update), update_value(this_alpha, alpha, 1.0, update), )) } else if alpha.is_some() { Arc::new(color.with_alpha(update_value(color.alpha(), alpha, 1.0, update))) } else { color }; Ok(Value::Color(color)) } pub(crate) fn scale_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Scale) } pub(crate) fn change_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Change) } pub(crate) fn adjust_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Adjust) } pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::String(color.to_ie_hex_str(), QuoteKind::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("change-color", Builtin::new(change_color)); f.insert("adjust-color", Builtin::new(adjust_color)); f.insert("scale-color", Builtin::new(scale_color)); f.insert("ie-hex-str", Builtin::new(ie_hex_str)); } grass-0.13.4/crates/compiler/src/builtin/functions/color/rgb.rs000066400000000000000000000311621465374720000245270ustar00rootroot00000000000000use crate::{builtin::builtin_imports::*, serializer::inspect_number, value::fuzzy_round}; use super::ParsedChannels; pub(crate) fn function_string( name: &'static str, args: &[Value], visitor: &mut Visitor, span: Span, ) -> SassResult { let args = args .iter() .map(|arg| arg.to_css_string(span, visitor.options.is_compressed())) .collect::>>()? .join(", "); Ok(format!("{}({})", name, args)) } fn inner_rgb_2_arg( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { // rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789` // and functions are parsed after variable substitution. let color = args.get_err(0, "color")?; let alpha = args.get_err(1, "alpha")?; let is_compressed = visitor.options.is_compressed(); if color.is_var() { return Ok(Value::String( function_string(name, &[color, alpha], visitor, args.span())?, QuoteKind::None, )); } else if alpha.is_var() { match &color { Value::Color(color) => { return Ok(Value::String( format!( "{}({}, {}, {}, {})", name, color.red().to_string(is_compressed), color.green().to_string(is_compressed), color.blue().to_string(is_compressed), alpha.to_css_string(args.span(), is_compressed)? ), QuoteKind::None, )); } _ => { return Ok(Value::String( function_string(name, &[color, alpha], visitor, args.span())?, QuoteKind::None, )) } } } else if alpha.is_special_function() { let color = color.assert_color_with_name("color", args.span())?; return Ok(Value::String( format!( "{}({}, {}, {}, {})", name, color.red().to_string(is_compressed), color.green().to_string(is_compressed), color.blue().to_string(is_compressed), alpha.to_css_string(args.span(), is_compressed)? ), QuoteKind::None, )); } let color = color.assert_color_with_name("color", args.span())?; let alpha = alpha.assert_number_with_name("alpha", args.span())?; Ok(Value::Color(Arc::new(color.with_alpha(Number( percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?, ))))) } fn inner_rgb_3_arg( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { let alpha = if args.len() > 3 { args.get(3, "alpha") } else { None }; let red = args.get_err(0, "red")?; let green = args.get_err(1, "green")?; let blue = args.get_err(2, "blue")?; if red.is_special_function() || green.is_special_function() || blue.is_special_function() || alpha .as_ref() .map(|alpha| alpha.node.is_special_function()) .unwrap_or(false) { let fn_string = if alpha.is_some() { function_string( name, &[red, green, blue, alpha.unwrap().node], visitor, args.span(), )? } else { function_string(name, &[red, green, blue], visitor, args.span())? }; return Ok(Value::String(fn_string, QuoteKind::None)); } let span = args.span(); let red = red.assert_number_with_name("red", span)?; let green = green.assert_number_with_name("green", span)?; let blue = blue.assert_number_with_name("blue", span)?; Ok(Value::Color(Arc::new(Color::from_rgba_fn( Number(fuzzy_round(percentage_or_unitless( &red, 255.0, "red", span, visitor, )?)), Number(fuzzy_round(percentage_or_unitless( &green, 255.0, "green", span, visitor, )?)), Number(fuzzy_round(percentage_or_unitless( &blue, 255.0, "blue", span, visitor, )?)), Number( alpha .map(|alpha| { percentage_or_unitless( &alpha.node.assert_number_with_name("alpha", span)?, 1.0, "alpha", span, visitor, ) }) .transpose()? .unwrap_or(1.0), ), )))) } pub(crate) fn percentage_or_unitless( number: &SassNumber, max: f64, name: &str, span: Span, visitor: &mut Visitor, ) -> SassResult { let value = if number.unit == Unit::None { number.num } else if number.unit == Unit::Percent { (number.num * Number(max)) / Number(100.0) } else { return Err(( format!( "${name}: Expected {} to have no units or \"%\".", inspect_number(number, visitor.options, span)?, name = name, ), span, ) .into()); }; Ok(value.clamp(0.0, max).0) } fn is_var_slash(value: &Value) -> bool { match value { Value::String(text, QuoteKind::Quoted) => { text.to_ascii_lowercase().starts_with("var(") && text.contains('/') } _ => false, } } pub(crate) fn parse_channels( name: &'static str, arg_names: &[&'static str], mut channels: Value, visitor: &mut Visitor, span: Span, ) -> SassResult { if channels.is_var() { let fn_string = function_string(name, &[channels], visitor, span)?; return Ok(ParsedChannels::String(fn_string)); } let original_channels = channels.clone(); let mut alpha_from_slash_list = None; if channels.separator() == ListSeparator::Slash { let list = channels.clone().as_list(); if list.len() != 2 { return Err(( format!( "Only 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 !inner_alpha_from_slash_list.is_special_function() { 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}.", argument = 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)), } } fn inner_rgb( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(4)?; match args.len() { 0 | 1 => { match parse_channels( name, &["red", "green", "blue"], args.get_err(0, "channels")?, visitor, args.span(), )? { ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, named: BTreeMap::new(), separator: ListSeparator::Comma, span: args.span(), touched: BTreeSet::new(), }; inner_rgb_3_arg(name, args, visitor) } } } 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 { inner_rgb("rgb", args, visitor) } pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_rgb("rgba", args, visitor) } pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.red()))) } pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.green()))) } pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.blue()))) } pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let color1 = args .get_err(0, "color1")? .assert_color_with_name("color1", args.span())?; let color2 = args .get_err(1, "color2")? .assert_color_with_name("color2", args.span())?; let weight = match args.default_arg( 2, "weight", Value::Dimension(SassNumber::new_unitless(50.0)), ) { Value::Dimension(mut num) => { num.assert_bounds("weight", 0.0, 100.0, args.span())?; num.num /= Number(100.0); num.num } v => { return Err(( format!( "$weight: {} is not a number.", v.to_css_string(args.span(), visitor.options.is_compressed())? ), args.span(), ) .into()) } }; Ok(Value::Color(Arc::new(color1.mix(&color2, weight)))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("rgb", Builtin::new(rgb)); f.insert("rgba", Builtin::new(rgba)); f.insert("red", Builtin::new(red)); f.insert("green", Builtin::new(green)); f.insert("blue", Builtin::new(blue)); f.insert("mix", Builtin::new(mix)); } grass-0.13.4/crates/compiler/src/builtin/functions/list.rs000066400000000000000000000203741465374720000236150ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let len = args.get_err(0, "list")?.as_list().len(); Ok(Value::Dimension(SassNumber::new_unitless(len))) } pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let mut list = args.get_err(0, "list")?.as_list(); let index = args .get_err(1, "n")? .assert_number_with_name("n", args.span())?; if index.num.is_zero() { return Err(("$n: List index may not be 0.", args.span()).into()); } if index.num.abs() > Number::from(list.len()) { return Err(( format!( "$n: Invalid index {}{} for a list with {} elements.", index.num.inspect(), index.unit, list.len() ), args.span(), ) .into()); } let index_int = index.assert_int_with_name("n", args.span())?; Ok(list.remove(if index.num.is_positive() { debug_assert!(index_int > 0); index_int as usize - 1 } else { list.len() - index_int.unsigned_abs() as usize })) } pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::String( args.get_err(0, "list")?.separator().name().to_owned(), QuoteKind::None, )) } pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), Value::ArgList(v) => ( v.elems.into_iter().collect(), ListSeparator::Comma, Brackets::None, ), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let index = args .get_err(1, "n")? .assert_number_with_name("n", args.span())?; if index.num.is_zero() { return Err(("$n: List index may not be 0.", args.span()).into()); } let index_int = index.assert_int_with_name("n", args.span())?; let len = list.len(); if index.num.abs() > Number::from(len) { return Err(( format!( "$n: Invalid index {}{} for a list with {} elements.", index.num.inspect(), index.unit, len ), args.span(), ) .into()); } let val = args.get_err(2, "value")?; if index_int.is_positive() { list[index_int as usize - 1] = val; } else { list[len - index_int.unsigned_abs() as usize] = val; } Ok(Value::List(list, sep, brackets)) } pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let val = args.get_err(1, "val")?; let sep = match args.default_arg( 2, "separator", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => { if sep == ListSeparator::Undecided { ListSeparator::Space } else { sep } } "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, "slash" => ListSeparator::Slash, _ => { return Err(( "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".", args.span(), ) .into()) } }, v => { return Err(( format!("$separator: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; list.push(val); Ok(Value::List(list, sep, brackets)) } pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(4)?; let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { Value::List(v, sep, brackets) => (v, sep, brackets), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let (list2, sep2) = match args.get_err(1, "list2")? { Value::List(v, sep, ..) => (v, sep), Value::Map(m) => (m.as_list(), ListSeparator::Comma), v => (vec![v], ListSeparator::Undecided), }; let sep = match args.default_arg( 2, "separator", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => { if sep1 != ListSeparator::Undecided { sep1 } else if sep2 != ListSeparator::Undecided { sep2 } else { ListSeparator::Space } } "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, "slash" => ListSeparator::Slash, _ => { return Err(( "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".", args.span(), ) .into()) } }, v => { return Err(( format!("$separator: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let brackets = match args.default_arg( 3, "bracketed", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => brackets, _ => Brackets::Bracketed, }, v => { if v.is_truthy() { Brackets::Bracketed } else { Brackets::None } } }; list1.extend(list2); Ok(Value::List(list1, sep, brackets)) } pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::bool(match args.get_err(0, "list")? { Value::List(.., brackets) => match brackets { Brackets::Bracketed => true, Brackets::None => false, }, _ => false, })) } pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let list = args.get_err(0, "list")?.as_list(); let value = args.get_err(1, "value")?; let index = match list.into_iter().position(|v| v == value) { Some(v) => v + 1, None => return Ok(Value::Null), }; Ok(Value::Dimension(SassNumber::new_unitless(index))) } pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let lists = args .get_variadic()? .into_iter() .map(|x| x.node.as_list()) .collect::>>(); let len = lists.iter().map(Vec::len).min().unwrap_or(0); if len == 0 { return Ok(Value::List( Vec::new(), ListSeparator::Comma, Brackets::None, )); } let result = (0..len) .map(|i| { let items = lists.iter().map(|v| v[i].clone()).collect(); Value::List(items, ListSeparator::Space, Brackets::None) }) .collect(); Ok(Value::List(result, ListSeparator::Comma, Brackets::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("length", Builtin::new(length)); f.insert("nth", Builtin::new(nth)); f.insert("list-separator", Builtin::new(list_separator)); f.insert("set-nth", Builtin::new(set_nth)); f.insert("append", Builtin::new(append)); f.insert("join", Builtin::new(join)); f.insert("is-bracketed", Builtin::new(is_bracketed)); f.insert("index", Builtin::new(index)); f.insert("zip", Builtin::new(zip)); } grass-0.13.4/crates/compiler/src/builtin/functions/map.rs000066400000000000000000000155531465374720000234220ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; /// map.get($map, $key, $keys...) /// map-get($map, $key, $keys...) /// /// If $keys is empty, returns the value in $map associated with $key. /// If $map doesn’t have a value associated with $key, returns null. /// If $keys is not empty, follows the set of keys including $key and /// excluding the last key in $keys, from left to right, to find the /// nested map targeted for searching. /// Returns the value in the targeted map associated with the last key /// in $keys. /// Returns null if the map does not have a value associated with the /// key, or if any key in $keys is missing from a map or references a /// value that is not a map. /// /// https://sass-lang.com/documentation/modules/map/ pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key = args.get_err(1, "key")?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; // since we already extracted the map and first key, // neither will be returned in the variadic args list let keys = args.get_variadic()?; let mut val = map.get(&key).unwrap_or(Value::Null); for key in keys { // if at any point we find a value that's not a map, // we return null let val_map = match val.try_map() { Some(val_map) => val_map, None => return Ok(Value::Null), }; val = val_map.get(&key).unwrap_or(Value::Null); } Ok(val) } pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key = args.get_err(1, "key")?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; // since we already extracted the map and first key, // neither will be returned in the variadic args list let keys = args.get_variadic()?; let mut val = match map.get(&key) { Some(v) => v, None => return Ok(Value::False), }; for key in keys { // if at any point we find a value that's not a map, // we return null let val_map = match val.try_map() { Some(val_map) => val_map, None => return Ok(Value::False), }; val = match val_map.get(&key) { Some(v) => v, None => return Ok(Value::False), }; } Ok(Value::True) } pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; Ok(Value::List( map.keys(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; Ok(Value::List( map.values(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { if args.len() == 1 { return Err(("Expected $args to contain a key.", args.span()).into()); } let map2_position = args.len().saturating_sub(1); let mut map1 = args .get_err(0, "map1")? .assert_map_with_name("map1", args.span())?; let map2 = args .get_err(map2_position, "map2")? .assert_map_with_name("map2", args.span())?; let keys = args.get_variadic()?; if keys.is_empty() { map1.merge(map2); } else { let mut current_map = map1.clone(); let mut map_queue = Vec::new(); for key in keys { match current_map.get(&key) { Some(Value::Map(m1)) => { current_map = m1.clone(); map_queue.push((key, m1)); } Some(..) | None => { current_map = SassMap::new(); map_queue.push((key, SassMap::new())); } } } match map_queue.last_mut() { Some((_, m)) => { m.merge(map2); } None => unreachable!(), }; while let Some((key, queued_map)) = map_queue.pop() { match map_queue.last_mut() { Some((_, map)) => { map.insert(key, Value::Map(queued_map)); } None => { map1.insert(key, Value::Map(queued_map)); break; } } } } Ok(Value::Map(map1)) } pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let mut map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; let keys = args.get_variadic()?; for key in keys { map.remove(&key); } Ok(Value::Map(map)) } pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key_position = args.len().saturating_sub(2); let value_position = args.len().saturating_sub(1); let mut map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; let key = Spanned { node: args.get_err(key_position, "key")?, span: args.span(), }; let value = args.get_err(value_position, "value")?; let keys = args.get_variadic()?; if keys.is_empty() { map.insert(key, value); } else { let mut current_map = map.clone(); let mut map_queue = Vec::new(); for key in keys { match current_map.get(&key) { Some(Value::Map(m1)) => { current_map = m1.clone(); map_queue.push((key, m1)); } Some(..) | None => { current_map = SassMap::new(); map_queue.push((key, SassMap::new())); } } } match map_queue.last_mut() { Some((_, m)) => m.insert(key, value), None => unreachable!(), }; while let Some((key, queued_map)) = map_queue.pop() { match map_queue.last_mut() { Some((_, next_map)) => { next_map.insert(key, Value::Map(queued_map)); } None => { map.insert(key, Value::Map(queued_map)); break; } } } } Ok(Value::Map(map)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("map-get", Builtin::new(map_get)); f.insert("map-has-key", Builtin::new(map_has_key)); f.insert("map-keys", Builtin::new(map_keys)); f.insert("map-values", Builtin::new(map_values)); f.insert("map-merge", Builtin::new(map_merge)); f.insert("map-remove", Builtin::new(map_remove)); } grass-0.13.4/crates/compiler/src/builtin/functions/math.rs000066400000000000000000000151611465374720000235710ustar00rootroot00000000000000use crate::{builtin::builtin_imports::*, evaluate::div}; pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let num = args .get_err(0, "number")? .assert_number_with_name("number", args.span)?; num.assert_no_units("number", args.span)?; Ok(Value::Dimension(SassNumber { num: Number(num.num.0 * 100.0), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.round(); Ok(Value::Dimension(number)) } pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.ceil(); Ok(Value::Dimension(number)) } pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.floor(); Ok(Value::Dimension(number)) } pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut num = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; num.num = num.num.abs(); Ok(Value::Dimension(num)) } pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let unit1 = args .get_err(0, "number1")? .assert_number_with_name("number1", args.span())? .unit; let unit2 = args .get_err(1, "number2")? .assert_number_with_name("number2", args.span())? .unit; Ok(Value::bool(unit1.comparable(&unit2))) } #[cfg(feature = "random")] pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let limit = args.default_arg(0, "limit", Value::Null); if matches!(limit, Value::Null) { let mut rng = rand::thread_rng(); return Ok(Value::Dimension(SassNumber::new_unitless( rng.gen_range(0.0..1.0), ))); } let limit = limit.assert_number_with_name("limit", args.span())?; let limit_int = limit.assert_int_with_name("limit", args.span())?; let limit = limit.num; if limit.is_one() { return Ok(Value::Dimension(SassNumber::new_unitless(1.0))); } if limit.is_zero() || limit.is_negative() { return Err(( format!("$limit: Must be greater than 0, was {}.", limit.inspect()), args.span(), ) .into()); } let mut rng = rand::thread_rng(); Ok(Value::Dimension(SassNumber::new_unitless( rng.gen_range(0..limit_int) + 1, ))) } pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { Value::Dimension(SassNumber { num: number, unit, as_slash: _, }) => Ok((number, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? .into_iter(); let mut min = match nums.next() { Some((n, u)) => (n, u), None => unreachable!(), }; for (num, unit) in nums { let lhs = Value::Dimension(SassNumber { num, unit: unit.clone(), as_slash: None, }); let rhs = Value::Dimension(SassNumber { num: (min.0), unit: min.1.clone(), as_slash: None, }); if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_truthy() { min = (num, unit); } } Ok(Value::Dimension(SassNumber { num: (min.0), unit: min.1, as_slash: None, })) } pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { Value::Dimension(SassNumber { num: number, unit, as_slash: _, }) => Ok((number, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? .into_iter(); let mut max = match nums.next() { Some((n, u)) => (n, u), None => unreachable!(), }; for (num, unit) in nums { let lhs = Value::Dimension(SassNumber { num, unit: unit.clone(), as_slash: None, }); let rhs = Value::Dimension(SassNumber { num: (max.0), unit: max.1.clone(), as_slash: None, }); if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)? .is_truthy() { max = (num, unit); } } Ok(Value::Dimension(SassNumber { num: (max.0), unit: max.1, as_slash: None, })) } pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let number1 = args.get_err(0, "number1")?; let number2 = args.get_err(1, "number2")?; div(number1, number2, visitor.options, args.span()) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("percentage", Builtin::new(percentage)); f.insert("round", Builtin::new(round)); f.insert("ceil", Builtin::new(ceil)); f.insert("floor", Builtin::new(floor)); f.insert("abs", Builtin::new(abs)); f.insert("min", Builtin::new(min)); f.insert("max", Builtin::new(max)); f.insert("comparable", Builtin::new(comparable)); #[cfg(feature = "random")] f.insert("random", Builtin::new(random)); } grass-0.13.4/crates/compiler/src/builtin/functions/meta.rs000066400000000000000000000251641465374720000235720ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; // todo: this should be a constant of some sort. we shouldn't be allocating this // every time pub(crate) fn if_arguments() -> ArgumentDeclaration { ArgumentDeclaration { args: vec![ Argument { name: Identifier::from("condition"), default: None, }, Argument { name: Identifier::from("if-true"), default: None, }, Argument { name: Identifier::from("if-false"), default: None, }, ], rest: None, } } fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; if args.get_err(0, "condition")?.is_truthy() { Ok(args.get_err(1, "if-true")?) } else { Ok(args.get_err(2, "if-false")?) } } pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let feature = args .get_err(0, "feature")? .assert_string_with_name("feature", args.span())? .0; #[allow(clippy::match_same_arms)] Ok(match feature.as_str() { // A local variable will shadow a global variable unless // `!global` is used. "global-variable-shadowing" => Value::True, // the @extend rule will affect selectors nested in pseudo-classes // like :not() "extend-selector-pseudoclass" => Value::True, // Full support for unit arithmetic using units defined in the // [Values and Units Level 3][] spec. "units-level-3" => Value::True, // The Sass `@error` directive is supported. "at-error" => Value::True, // The "Custom Properties Level 1" spec is supported. This means // that custom properties are parsed statically, with only // interpolation treated as SassScript. "custom-property" => Value::True, _ => Value::False, }) } pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; Ok(Value::String(number.unit.to_string(), QuoteKind::Quoted)) } pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let value = args.get_err(0, "value")?; Ok(Value::String(value.kind().to_owned(), QuoteKind::None)) } pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; Ok(Value::bool(number.unit == Unit::None)) } pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::String( args.get_err(0, "value")?.inspect(args.span())?, QuoteKind::None, )) } pub(crate) fn variable_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(1)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); Ok(Value::bool(visitor.env.var_exists(name, None)?)) } pub(crate) fn global_variable_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .var_exists(name) } else { (*visitor.env.global_vars()).borrow().contains_key(&name) })) } pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .mixin_exists(name) } else { visitor.env.mixin_exists(name) })) } pub(crate) fn function_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .fn_exists(name) } else { visitor.env.fn_exists(name) })) } pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let name: Identifier = match args.get_err(0, "name")? { Value::String(s, _) => s.into(), v => { return Err(( format!("$name: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let css = args.default_arg(1, "css", Value::False).is_truthy(); let module = match args.default_arg(2, "module", Value::Null) { Value::String(s, ..) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; if css && module.is_some() { return Err(( "$css and $module may not both be passed at once.", args.span(), ) .into()); } let func = if css { Some(SassFunction::Plain { name }) } else if let Some(module_name) = module { visitor.env.get_fn( name, Some(Spanned { node: module_name.into(), span: args.span(), }), )? } else { match visitor.env.get_fn(name, None)? { Some(f) => Some(f), None => GLOBAL_FUNCTIONS .get(name.as_str()) .map(|f| SassFunction::Builtin(f.clone(), name)), } }; match func { Some(func) => Ok(Value::FunctionRef(Box::new(func))), None => Err((format!("Function not found: {}", name), args.span()).into()), } } pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let func = match args.get_err(0, "function")? { Value::FunctionRef(f) => *f, Value::String(name, ..) => { let name = Identifier::from(name); match visitor.env.get_fn(name, None)? { Some(f) => f, None => match GLOBAL_FUNCTIONS.get(name.as_str()) { Some(f) => SassFunction::Builtin(f.clone(), name), None => SassFunction::Plain { name }, }, } } v => { return Err(( format!( "$function: {} is not a function reference.", v.inspect(span)? ), span, ) .into()) } }; args.remove_positional(0); visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span) } #[allow(clippy::needless_pass_by_value)] pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(0)?; if !visitor.flags.in_mixin() { return Err(( "content-exists() may only be called within a mixin.", args.span(), ) .into()); } Ok(Value::bool(visitor.env.content.is_some())) } pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let args = match args.get_err(0, "args")? { Value::ArgList(args) => args, v => { return Err(( format!("$args: {} is not an argument list.", v.inspect(span)?), span, ) .into()) } }; Ok(Value::Map(SassMap::new_with( args.into_keywords() .into_iter() .map(|(name, val)| { ( Value::String(name.to_string(), QuoteKind::None).span(span), val, ) }) .collect(), ))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("if", Builtin::new(if_)); f.insert("feature-exists", Builtin::new(feature_exists)); f.insert("unit", Builtin::new(unit)); f.insert("type-of", Builtin::new(type_of)); f.insert("unitless", Builtin::new(unitless)); f.insert("inspect", Builtin::new(inspect)); f.insert("variable-exists", Builtin::new(variable_exists)); f.insert( "global-variable-exists", Builtin::new(global_variable_exists), ); f.insert("mixin-exists", Builtin::new(mixin_exists)); f.insert("function-exists", Builtin::new(function_exists)); f.insert("get-function", Builtin::new(get_function)); f.insert("call", Builtin::new(call)); f.insert("content-exists", Builtin::new(content_exists)); f.insert("keywords", Builtin::new(keywords)); } grass-0.13.4/crates/compiler/src/builtin/functions/mod.rs000066400000000000000000000057011465374720000234160ustar00rootroot00000000000000// A reference to the parser is only necessary for some functions #![allow(unused_variables)] use std::{ collections::{BTreeSet, HashMap}, fmt, sync::atomic::{AtomicUsize, Ordering}, }; use once_cell::sync::Lazy; use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value}; pub mod color; pub mod list; pub mod map; pub mod math; pub mod meta; pub mod selector; pub mod string; // todo: maybe Identifier instead of str? pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>; static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0); /// A function implemented in rust that is accessible from within Sass /// /// /// #### Usage /// ```rust /// use grass_compiler::{ /// sass_value::{ArgumentResult, SassNumber, Value}, /// Builtin, Options, Result as SassResult, Visitor, /// }; /// /// // An example function that looks up the length of an array or map and adds 2 to it /// fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { /// args.max_args(1)?; /// /// let len = args.get_err(0, "list")?.as_list().len(); /// /// Ok(Value::Dimension(SassNumber::new_unitless(len + 2))) /// } /// /// fn main() { /// let options = Options::default().add_custom_fn("length", Builtin::new(length)); /// let css = grass_compiler::from_string("a { color: length([a, b]); }", &options).unwrap(); /// /// assert_eq!(css, "a {\n color: 4;\n}\n"); /// } /// ``` #[derive(Clone)] pub struct Builtin( pub(crate) fn(ArgumentResult, &mut Visitor) -> SassResult, usize, ); impl fmt::Debug for Builtin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Builtin") .field("id", &self.1) .field("fn_ptr", &(self.0 as usize)) .finish() } } impl Builtin { pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult) -> Builtin { let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); Self(body, count) } } impl PartialEq for Builtin { fn eq(&self, other: &Self) -> bool { self.1 == other.1 } } impl Eq for Builtin {} pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { let mut m = HashMap::new(); color::declare(&mut m); list::declare(&mut m); map::declare(&mut m); math::declare(&mut m); meta::declare(&mut m); selector::declare(&mut m); string::declare(&mut m); m }); pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy> = Lazy::new(|| { GLOBAL_FUNCTIONS .keys() .copied() .filter(|&name| { !matches!( name, "rgb" | "rgba" | "hsl" | "hsla" | "grayscale" | "invert" | "alpha" | "opacity" | "saturate" ) }) .collect() }); grass-0.13.4/crates/compiler/src/builtin/functions/selector.rs000066400000000000000000000176271465374720000244710ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; use crate::selector::{ ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList, }; use crate::serializer::serialize_selector_list; pub(crate) fn is_superselector( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let parent_selector = args.get_err(0, "super")? .to_selector(visitor, "super", false, args.span())?; let child_selector = args .get_err(1, "sub")? .to_selector(visitor, "sub", false, args.span())?; Ok(Value::bool( parent_selector.is_super_selector(&child_selector), )) } pub(crate) fn simple_selectors( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(1)?; // todo: Value::to_compound_selector let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span())?; if selector.0.components.len() != 1 { return Err(("$selector: expected selector.", args.span()).into()); } let compound = if let Some(ComplexSelectorComponent::Compound(compound)) = selector.0.components[0].components.first().cloned() { compound } else { todo!() }; Ok(Value::List( compound .components .into_iter() .map(|simple| Value::String(simple.to_string(), QuoteKind::None)) .collect(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(args .get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span()) .map_err(|_| ("$selector: expected selector.", args.span()))? .into_value()) } pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { return Err(("$selectors: At least one selector must be passed.", span).into()); } Ok(selectors .into_iter() .map(|sel| sel.node.to_selector(visitor, "selectors", true, span)) .collect::>>()? .into_iter() .try_fold( Selector::new(span), |parent, child| -> SassResult { child.resolve_parent_selectors(&parent, true) }, )? .into_value()) } pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { return Err(("$selectors: At least one selector must be passed.", span).into()); } let mut parsed_selectors = selectors .into_iter() .map(|s| s.node.to_selector(visitor, "selectors", false, span)) .collect::>>()?; let first = parsed_selectors.remove(0); Ok(parsed_selectors .into_iter() .try_fold(first, |parent, child| -> SassResult { Selector(SelectorList { components: child .0 .components .into_iter() .map(|complex| -> SassResult { let compound = complex.components.first(); if let Some(ComplexSelectorComponent::Compound(compound)) = compound { let mut components = vec![match compound.clone().prepend_parent() { Some(v) => ComplexSelectorComponent::Compound(v), None => { return Err(( format!( "Can't append {} to {}.", complex, serialize_selector_list( &parent.0, visitor.options, span ) ), span, ) .into()) } }]; components.extend(complex.components.into_iter().skip(1)); Ok(ComplexSelector::new(components, false)) } else { Err(( format!( "Can't append {} to {}.", complex, serialize_selector_list(&parent.0, visitor.options, span) ), span, ) .into()) } }) .collect::>>()?, span, }) .resolve_parent_selectors(&parent, false) })? .into_value()) } pub(crate) fn selector_extend( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(3)?; let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span())?; let target = args.get_err(1, "extendee")? .to_selector(visitor, "extendee", false, args.span())?; let source = args.get_err(2, "extender")? .to_selector(visitor, "extender", false, args.span())?; Ok(ExtensionStore::extend(selector.0, source.0, target.0, args.span())?.to_sass_list()) } pub(crate) fn selector_replace( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(3)?; let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", true, args.span())?; let target = args.get_err(1, "original")? .to_selector(visitor, "original", true, args.span())?; let source = args.get_err(2, "replacement")? .to_selector(visitor, "replacement", true, args.span())?; Ok(ExtensionStore::replace(selector.0, source.0, target.0, args.span())?.to_sass_list()) } pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let selector1 = args.get_err(0, "selector1")? .to_selector(visitor, "selector1", true, args.span())?; if selector1.contains_parent_selector() { return Err(( "$selector1: Parent selectors aren't allowed here.", args.span(), ) .into()); } let selector2 = args.get_err(1, "selector2")? .to_selector(visitor, "selector2", true, args.span())?; if selector2.contains_parent_selector() { return Err(( "$selector2: Parent selectors aren't allowed here.", args.span(), ) .into()); } Ok(match selector1.unify(&selector2) { Some(sel) => sel.into_value(), None => Value::Null, }) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("is-superselector", Builtin::new(is_superselector)); f.insert("simple-selectors", Builtin::new(simple_selectors)); f.insert("selector-parse", Builtin::new(selector_parse)); f.insert("selector-nest", Builtin::new(selector_nest)); f.insert("selector-append", Builtin::new(selector_append)); f.insert("selector-extend", Builtin::new(selector_extend)); f.insert("selector-replace", Builtin::new(selector_replace)); f.insert("selector-unify", Builtin::new(selector_unify)); } grass-0.13.4/crates/compiler/src/builtin/functions/string.rs000066400000000000000000000171271465374720000241520ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let (mut s, q) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; s.make_ascii_uppercase(); Ok(Value::String(s, q)) } pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let (mut s, q) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; s.make_ascii_lowercase(); Ok(Value::String(s, q)) } pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::Dimension(SassNumber::new_unitless( s.chars().count(), ))) } pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::String(s, QuoteKind::Quoted)) } pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::String(s, QuoteKind::None)) } pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let (string, quotes) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; let str_len = string.chars().count(); let start = args .get_err(1, "start-at")? .assert_number_with_name("start-at", span)?; start.assert_no_units("start-at", span)?; let start = start.num.assert_int(span)?; let start = if start == 0 { 1 } else if start > 0 { (start as usize).min(str_len + 1) } else { (start + str_len as i64 + 1).max(1) as usize }; let end = args .default_arg( 2, "end-at", Value::Dimension(SassNumber::new_unitless(-1.0)), ) .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 i64 + 1; } let end = (end.max(0) as usize).min(str_len + 1); if start > end || start > str_len { Ok(Value::String(String::new(), quotes)) } else { Ok(Value::String( string .chars() .skip(start - 1) .take(end - start + 1) .collect(), quotes, )) } } /// https://sass-lang.com/documentation/modules/string/#split /// /// Returns a bracketed, comma-separated list of substrings of $string /// that are separated by $separator. The $separators aren’t included /// in these substrings. /// /// If $limit is a number 1 or higher, this splits on at most that many /// $separators (and so returns at most $limit + 1 strings). The last /// substring contains the rest of the string, including any remaining /// $separators. pub(crate) fn str_split(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let s1 = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; let separator = args .get_err(1, "separator")? .assert_string_with_name("separator", args.span())? .0; let limit = args.default_arg(2, "limit", Value::Null); let vec = if matches!(limit, Value::Null) { s1.split(&separator) .map(|s| Value::String(s.to_string(), QuoteKind::Quoted)) .collect() } else { let limit = limit.assert_number_with_name("limit", args.span())?; let limit_int = limit.assert_int_with_name("limit", args.span())?; if limit_int < 1 { return Err(( format!("$limit: Must be 1 or greater, was {}.", limit_int), args.span(), ) .into()); } // note: `1 + limit_int` is required to match dart-sass s1.splitn(1 + limit_int as usize, &separator) .map(|s| Value::String(s.to_string(), QuoteKind::Quoted)) .collect() }; Ok(Value::List(vec, ListSeparator::Comma, Brackets::Bracketed)) } pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let s1 = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; let substr = args .get_err(1, "substring")? .assert_string_with_name("substring", args.span())? .0; let char_position = match s1.find(&substr) { Some(i) => s1[0..i].chars().count() + 1, None => return Ok(Value::Null), }; Ok(Value::Dimension(SassNumber::new_unitless(char_position))) } pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let (s1, quotes) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; let substr = args .get_err(1, "insert")? .assert_string_with_name("insert", args.span())? .0; let index = args .get_err(2, "index")? .assert_number_with_name("index", span)?; index.assert_no_units("index", span)?; let index_int = index.assert_int_with_name("index", span)?; if s1.is_empty() { return Ok(Value::String(substr, quotes)); } let len = s1.chars().count(); // Insert substring at char position, rather than byte position let insert = |idx, s1: String, s2| { s1.chars() .enumerate() .map(|(i, c)| { if i + 1 == idx { c.to_string() + s2 } else if idx == 0 && i == 0 { s2.to_owned() + &c.to_string() } else { c.to_string() } }) .collect::() }; let string = if index_int > 0 { insert((index_int as usize - 1).min(len), s1, &substr) } else if index_int == 0 { insert(0, s1, &substr) } else { let idx = (len as i64 + index_int + 1).max(0) as usize; insert(idx, s1, &substr) }; Ok(Value::String(string, quotes)) } #[cfg(feature = "random")] #[allow(clippy::needless_pass_by_value)] pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(0)?; let mut rng = thread_rng(); let string: String = std::iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .map(char::from) .take(12) .collect(); Ok(Value::String(format!("id-{}", string), QuoteKind::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("to-upper-case", Builtin::new(to_upper_case)); f.insert("to-lower-case", Builtin::new(to_lower_case)); f.insert("str-length", Builtin::new(str_length)); f.insert("quote", Builtin::new(quote)); f.insert("unquote", Builtin::new(unquote)); f.insert("str-slice", Builtin::new(str_slice)); f.insert("str-index", Builtin::new(str_index)); f.insert("str-insert", Builtin::new(str_insert)); #[cfg(feature = "random")] f.insert("unique-id", Builtin::new(unique_id)); } grass-0.13.4/crates/compiler/src/builtin/mod.rs000066400000000000000000000017751465374720000214150ustar00rootroot00000000000000mod functions; pub(crate) mod modules; pub(crate) use functions::{ color, list, map, math, meta, selector, string, DISALLOWED_PLAIN_CSS_FUNCTION_NAMES, GLOBAL_FUNCTIONS, }; pub use functions::Builtin; /// 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}, sync::Arc, }; } grass-0.13.4/crates/compiler/src/builtin/modules/000077500000000000000000000000001465374720000217265ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/builtin/modules/color.rs000066400000000000000000000021251465374720000234120ustar00rootroot00000000000000use crate::builtin::{ color::{ hsl::{complement, grayscale, hue, invert, lightness, saturation}, hwb::{blackness, hwb, whiteness}, opacity::alpha, other::{adjust_color, change_color, ie_hex_str, scale_color}, rgb::{blue, green, mix, red}, }, modules::Module, }; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("adjust", adjust_color); f.insert_builtin("alpha", alpha); f.insert_builtin("blue", blue); f.insert_builtin("change", change_color); f.insert_builtin("complement", complement); f.insert_builtin("grayscale", grayscale); f.insert_builtin("green", green); f.insert_builtin("hue", hue); f.insert_builtin("ie-hex-str", ie_hex_str); f.insert_builtin("invert", invert); f.insert_builtin("lightness", lightness); f.insert_builtin("mix", mix); f.insert_builtin("red", red); f.insert_builtin("saturation", saturation); f.insert_builtin("scale", scale_color); f.insert_builtin("blackness", blackness); f.insert_builtin("whiteness", whiteness); f.insert_builtin("hwb", hwb); } grass-0.13.4/crates/compiler/src/builtin/modules/list.rs000066400000000000000000000022301465374720000232440ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; use crate::builtin::{ list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip}, modules::Module, }; // todo: write tests for this fn slash(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let list = if args.len() == 1 { args.get_err(0, "elements")?.as_list() } else { args.get_variadic()? .into_iter() .map(|arg| arg.node) .collect() }; if list.len() < 2 { return Err(("At least two elements are required.", span).into()); } Ok(Value::List(list, ListSeparator::Slash, Brackets::None)) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("append", append); f.insert_builtin("index", index); f.insert_builtin("is-bracketed", is_bracketed); f.insert_builtin("join", join); f.insert_builtin("length", length); f.insert_builtin("separator", list_separator); f.insert_builtin("nth", nth); f.insert_builtin("set-nth", set_nth); f.insert_builtin("zip", zip); f.insert_builtin("slash", slash); } grass-0.13.4/crates/compiler/src/builtin/modules/map.rs000066400000000000000000000077061465374720000230630ustar00rootroot00000000000000use std::iter::Peekable; use crate::builtin::builtin_imports::*; use crate::builtin::{ map::{map_get, map_has_key, map_keys, map_merge, map_remove, map_set, map_values}, modules::Module, }; fn deep_merge_impl(map1: SassMap, map2: SassMap) -> SassMap { if map1.is_empty() { return map2; } if map2.is_empty() { return map1; } let mut result = map1; for (key, value) in map2 { match result.get_ref(&key.node).and_then(Value::try_map) { Some(result_map) => match value.try_map() { Some(value_map) => { let merged = deep_merge_impl(result_map, value_map); result.insert(key, Value::Map(merged)); } None => { result.insert(key, value); } }, None => { result.insert(key, value); } } } result } fn deep_merge(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let map1 = args .get_err(0, "map1")? .assert_map_with_name("map1", span)?; let map2 = args .get_err(1, "map2")? .assert_map_with_name("map2", span)?; Ok(Value::Map(deep_merge_impl(map1, map2))) } fn deep_remove(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { let span = args.span(); let map = args.get_err(0, "map")?.assert_map_with_name("map", span)?; let key = args.get_err(1, "key")?; let keys = args.get_variadic()?.into_iter().map(|arg| arg.node); let mut keys = std::iter::once(key).chain(keys).collect::>(); let last = keys.pop(); let map = modify_map( map, keys.into_iter(), |value| { let last = match last.as_ref() { Some(v) => v, None => return value, }; match value.try_map() { Some(mut nested_map) if nested_map.contains(last) => { nested_map.remove(last); Value::Map(nested_map) } Some(..) | None => value, } }, false, span, ); Ok(map) } fn modify_map( map: SassMap, keys: impl Iterator, modify: impl Fn(Value) -> Value, // default=true add_nesting: bool, span: Span, ) -> Value { let mut keys = keys.peekable(); fn modify_nested_map( mut mutable_map: SassMap, mut keys: Peekable>, add_nesting: bool, span: Span, modify: impl Fn(Value) -> Value, ) -> SassMap { let key = keys.next().unwrap(); if keys.peek().is_none() { let value = modify(mutable_map.get_ref(&key).cloned().unwrap_or(Value::Null)); mutable_map.insert(key.span(span), value); return mutable_map; } let nested_map = mutable_map.get_ref(&key).and_then(|v| v.try_map()); if nested_map.is_none() && !add_nesting { return mutable_map; } mutable_map.insert( key.span(span), Value::Map(modify_nested_map( nested_map.unwrap_or_default(), keys, add_nesting, span, modify, )), ); mutable_map } if keys.peek().is_some() { Value::Map(modify_nested_map(map, keys, add_nesting, span, modify)) } else { modify(Value::Map(map)) } } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("get", map_get); f.insert_builtin("has-key", map_has_key); f.insert_builtin("keys", map_keys); f.insert_builtin("merge", map_merge); f.insert_builtin("remove", map_remove); f.insert_builtin("values", map_values); f.insert_builtin("set", map_set); f.insert_builtin("deep-merge", deep_merge); f.insert_builtin("deep-remove", deep_remove); } grass-0.13.4/crates/compiler/src/builtin/modules/math.rs000066400000000000000000000346231465374720000232350ustar00rootroot00000000000000use crate::builtin::builtin_imports::*; use crate::builtin::{ math::{abs, ceil, comparable, divide, floor, max, min, percentage, round}, meta::{unit, unitless}, modules::Module, }; #[cfg(feature = "random")] use crate::builtin::math::random; use crate::value::{conversion_factor, SassNumber}; fn coerce_to_rad(num: f64, unit: Unit) -> f64 { debug_assert!(matches!( unit, Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn )); if unit == Unit::None { return num; } let factor = conversion_factor(&unit, &Unit::Rad).unwrap(); num * factor } fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let min = match args.get_err(0, "min")? { v @ Value::Dimension(SassNumber { .. }) => v, v => { return Err(( format!("$min: {} is not a number.", v.inspect(args.span())?), span, ) .into()) } }; let number = match args.get_err(1, "number")? { v @ Value::Dimension(SassNumber { .. }) => v, v => { return Err(( format!("$number: {} is not a number.", v.inspect(span)?), span, ) .into()) } }; let max = match args.get_err(2, "max")? { v @ Value::Dimension(SassNumber { .. }) => v, v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()), }; // ensure that `min` and `max` are compatible min.cmp(&max, span, BinaryOp::LessThan)?; let min_unit = match min { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; let number_unit = match number { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; let max_unit = match max { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; if min_unit == &Unit::None && number_unit != &Unit::None { return Err(( format!( "$min is unitless but $number has unit {}. Arguments must all have units or all be unitless.", number_unit ), span).into()); } else if min_unit != &Unit::None && number_unit == &Unit::None { return Err(( format!( "$min has unit {} but $number is unitless. Arguments must all have units or all be unitless.", min_unit ), span).into()); } else if min_unit != &Unit::None && max_unit == &Unit::None { return Err(( format!( "$min has unit {} but $max is unitless. Arguments must all have units or all be unitless.", min_unit ), span).into()); } match min.cmp(&number, span, BinaryOp::LessThan)? { Some(Ordering::Greater) => return Ok(min), Some(Ordering::Equal) => return Ok(number), Some(Ordering::Less) | None => {} } match max.cmp(&number, span, BinaryOp::GreaterThan)? { Some(Ordering::Less) => return Ok(max), Some(Ordering::Equal) => return Ok(number), Some(Ordering::Greater) | None => {} } Ok(number) } fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> { match v.node { Value::Dimension(SassNumber { num, unit, .. }) => Ok((num, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), } }); let (n, u) = numbers.next().unwrap()?; let first: (Number, Unit) = (n * n, u); let rest = numbers .enumerate() .map(|(idx, val)| -> SassResult { let (number, unit) = val?; if first.1 == Unit::None { if unit == Unit::None { Ok(number * number) } else { Err(( format!( "Argument 1 is unitless but argument {} has unit {}. \ Arguments must all have units or all be unitless.", idx + 2, unit ), span, ) .into()) } } else if unit == Unit::None { Err(( format!( "Argument 1 has unit {} but argument {} is unitless. \ Arguments must all have units or all be unitless.", first.1, idx + 2, ), span, ) .into()) } else if first.1.comparable(&unit) { let n = number.convert(&unit, &first.1); Ok(n * n) } else { Err(( format!("Incompatible units {} and {}.", first.1, unit), span, ) .into()) } }) .collect::>>()?; let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b); Ok(Value::Dimension(SassNumber { num: sum.sqrt(), unit: first.1, as_slash: None, })) } fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; let base = match args.default_arg(1, "base", Value::Null) { Value::Null => None, v => { let base = v.assert_number_with_name("base", span)?; base.assert_no_units("base", span)?; Some(base.num) } }; Ok(Value::Dimension(SassNumber::new_unitless( if let Some(base) = base { if base.is_zero() { Number::zero() } else { number.log(base) } // todo: test with negative 0 } else if number.is_negative() && !number.is_zero() { Number(f64::NAN) } else if number.is_zero() { Number(f64::NEG_INFINITY) } else { number.ln() }, ))) } fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let base = args .get_err(0, "base")? .assert_number_with_name("base", span)?; base.assert_no_units("base", span)?; let exponent = args .get_err(1, "exponent")? .assert_number_with_name("exponent", span)?; exponent.assert_no_units("exponent", span)?; Ok(Value::Dimension(SassNumber::new_unitless( base.num.pow(exponent.num), ))) } fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; number.assert_no_units("number", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless( number.num.sqrt(), ))) } macro_rules! trig_fn { ($name:ident) => { fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args.get_err(0, "number")?; Ok(match number { Value::Dimension(SassNumber { num, unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn), .. }) => { Value::Dimension(SassNumber::new_unitless(coerce_to_rad(num.0, unit).$name())) } v @ Value::Dimension(..) => { return Err(( format!( "$number: Expected {} to have an angle unit (deg, grad, rad, turn).", v.inspect(args.span())? ), args.span(), ) .into()) } v => { return Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }) } }; } trig_fn!(cos); trig_fn!(sin); trig_fn!(tan); fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; Ok(Value::Dimension(SassNumber { num: if number > Number(1.0) || number < Number(-1.0) { Number(f64::NAN) } else if number.is_one() { Number::zero() } else { number.acos() }, unit: Unit::Deg, as_slash: None, })) } fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; if number > Number(1.0) || number < Number(-1.0) { return Ok(Value::Dimension(SassNumber { num: Number(f64::NAN), unit: Unit::Deg, as_slash: None, })); } else if number.is_zero() { return Ok(Value::Dimension(SassNumber { num: Number::zero(), unit: Unit::Deg, as_slash: None, })); } Ok(Value::Dimension(SassNumber { num: number.asin(), unit: Unit::Deg, as_slash: None, })) } fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; if number.num.is_zero() { return Ok(Value::Dimension(SassNumber { num: (Number::zero()), unit: Unit::Deg, as_slash: None, })); } Ok(Value::Dimension(SassNumber { num: number.num.atan(), unit: Unit::Deg, as_slash: None, })) } fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let (y_num, y_unit) = match args.get_err(0, "y")? { Value::Dimension(SassNumber { num: n, unit: u, .. }) => (n, u), v => { return Err(( format!("$y: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; let (x_num, x_unit) = match args.get_err(1, "x")? { Value::Dimension(SassNumber { num: n, unit: u, .. }) => (n, u), v => { return Err(( format!("$x: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None { (x_num, y_num) } else if y_unit == Unit::None { return Err(( format!( "$y is unitless but $x has unit {}. \ Arguments must all have units or all be unitless.", x_unit ), args.span(), ) .into()); } else if x_unit == Unit::None { return Err(( format!( "$y has unit {} but $x is unitless. \ Arguments must all have units or all be unitless.", y_unit ), args.span(), ) .into()); } else if x_unit.comparable(&y_unit) { (x_num, y_num.convert(&y_unit, &x_unit)) } else { return Err(( format!("Incompatible units {} and {}.", y_unit, x_unit), args.span(), ) .into()); }; Ok(Value::Dimension(SassNumber { num: Number(y_num.0.atan2(x_num.0).to_degrees()), unit: Unit::Deg, as_slash: None, })) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("ceil", ceil); f.insert_builtin("floor", floor); f.insert_builtin("max", max); f.insert_builtin("min", min); f.insert_builtin("round", round); f.insert_builtin("abs", abs); f.insert_builtin("compatible", comparable); f.insert_builtin("is-unitless", unitless); f.insert_builtin("unit", unit); f.insert_builtin("percentage", percentage); f.insert_builtin("clamp", clamp); f.insert_builtin("sqrt", sqrt); f.insert_builtin("cos", cos); f.insert_builtin("sin", sin); f.insert_builtin("tan", tan); f.insert_builtin("acos", acos); f.insert_builtin("asin", asin); f.insert_builtin("atan", atan); f.insert_builtin("log", log); f.insert_builtin("pow", pow); f.insert_builtin("hypot", hypot); f.insert_builtin("div", divide); f.insert_builtin("atan2", atan2); #[cfg(feature = "random")] f.insert_builtin("random", random); f.insert_builtin_var( "e", Value::Dimension(SassNumber::new_unitless(std::f64::consts::E)), ); f.insert_builtin_var( "pi", Value::Dimension(SassNumber::new_unitless(std::f64::consts::PI)), ); f.insert_builtin_var( "epsilon", Value::Dimension(SassNumber::new_unitless(f64::EPSILON)), ); f.insert_builtin_var( "max-safe-integer", Value::Dimension(SassNumber::new_unitless(9007199254740991.0)), ); f.insert_builtin_var( "min-safe-integer", Value::Dimension(SassNumber::new_unitless(-9007199254740991.0)), ); f.insert_builtin_var( "max-number", Value::Dimension(SassNumber::new_unitless(f64::MAX)), ); f.insert_builtin_var( "min-number", Value::Dimension(SassNumber::new_unitless(f64::MIN_POSITIVE)), ); } grass-0.13.4/crates/compiler/src/builtin/modules/meta.rs000066400000000000000000000135171465374720000232310ustar00rootroot00000000000000use std::cell::RefCell; use std::collections::BTreeMap; use std::sync::Arc; use crate::ast::{Configuration, ConfiguredValue}; use crate::builtin::builtin_imports::*; use crate::builtin::{ meta::{ call, content_exists, feature_exists, function_exists, get_function, global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists, }, modules::Module, }; use crate::serializer::serialize_calculation_arg; fn load_css(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<()> { args.max_args(2)?; let span = args.span(); let url = args .get_err(0, "module")? .assert_string_with_name("module", args.span())? .0; let with = match args.default_arg(1, "with", Value::Null) { Value::Map(map) => Some(map), Value::List(v, ..) if v.is_empty() => Some(SassMap::new()), Value::ArgList(v) if v.is_empty() => Some(SassMap::new()), Value::Null => None, v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()), }; let mut configuration = Configuration::empty(); if let Some(with) = with { visitor.emit_warning("`grass` does not currently support the $with parameter of load-css. This file will be imported the same way it would using `@import`.", args.span()); let mut values = BTreeMap::new(); for (key, value) in with { let name = Identifier::from(key.node.assert_string_with_name("with key", args.span())?.0); if values.contains_key(&name) { // todo: write test for this return Err(( format!("The variable {name} was configured twice.", name = name), key.span, ) .into()); } values.insert(name, ConfiguredValue::explicit(value, args.span())); } configuration = Configuration::explicit(values, args.span()); } let _configuration = Arc::new(RefCell::new(configuration)); let style_sheet = visitor.load_style_sheet(url.as_ref(), false, args.span())?; visitor.visit_stylesheet(style_sheet)?; // todo: support the $with argument to load-css // visitor.load_module( // url.as_ref(), // Some(Arc::clone(&configuration)), // true, // args.span(), // |visitor, module, stylesheet| { // // (*module).borrow() // Ok(()) // }, // )?; // Visitor::assert_configuration_is_empty(&configuration, true)?; Ok(()) } fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let module = Identifier::from( args.get_err(0, "module")? .assert_string_with_name("module", args.span())? .0, ); Ok(Value::Map( (*(*visitor.env.modules).borrow().get(module, args.span())?) .borrow() .functions(args.span()), )) } fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let module = Identifier::from( args.get_err(0, "module")? .assert_string_with_name("module", args.span())? .0, ); Ok(Value::Map( (*(*visitor.env.modules).borrow().get(module, args.span())?) .borrow() .variables(args.span()), )) } fn calc_args(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let calc = match args.get_err(0, "calc")? { Value::Calculation(calc) => calc, v => { return Err(( format!("$calc: {} is not a calculation.", v.inspect(args.span())?), args.span(), ) .into()) } }; let args = calc .args .into_iter() .map(|arg| { Ok(match arg { CalculationArg::Number(num) => Value::Dimension(num), CalculationArg::Calculation(calc) => Value::Calculation(calc), CalculationArg::String(s) | CalculationArg::Interpolation(s) => { Value::String(s, QuoteKind::None) } CalculationArg::Operation { .. } => Value::String( serialize_calculation_arg(&arg, visitor.options, args.span())?, QuoteKind::None, ), }) }) .collect::>>()?; Ok(Value::List(args, ListSeparator::Comma, Brackets::None)) } fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let calc = match args.get_err(0, "calc")? { Value::Calculation(calc) => calc, v => { return Err(( format!("$calc: {} is not a calculation.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::String(calc.name.to_string(), QuoteKind::Quoted)) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("feature-exists", feature_exists); f.insert_builtin("inspect", inspect); f.insert_builtin("type-of", type_of); f.insert_builtin("keywords", keywords); f.insert_builtin("global-variable-exists", global_variable_exists); f.insert_builtin("variable-exists", variable_exists); f.insert_builtin("function-exists", function_exists); f.insert_builtin("mixin-exists", mixin_exists); f.insert_builtin("content-exists", content_exists); f.insert_builtin("module-variables", module_variables); f.insert_builtin("module-functions", module_functions); f.insert_builtin("get-function", get_function); f.insert_builtin("call", call); f.insert_builtin("calc-args", calc_args); f.insert_builtin("calc-name", calc_name); f.insert_builtin_mixin("load-css", load_css); } grass-0.13.4/crates/compiler/src/builtin/modules/mod.rs000066400000000000000000000361661465374720000230670ustar00rootroot00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, fmt, sync::Arc, }; use codemap::{Span, Spanned}; use crate::{ ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin}, builtin::Builtin, common::Identifier, error::SassResult, evaluate::{Environment, Visitor}, selector::ExtensionStore, utils::{ BaseMapView, LimitedMapView, MapView, MergedMapView, PrefixedMapView, PublicMemberMapView, }, value::{SassFunction, SassMap, Value}, }; use super::builtin_imports::QuoteKind; mod color; mod list; mod map; mod math; mod meta; mod selector; mod string; /// A [Module] that only exposes members that aren't shadowed by a given /// blocklist of member names. #[derive(Debug, Clone)] pub(crate) struct ShadowedModule { #[allow(dead_code)] inner: Arc>, scope: ModuleScope, } impl ShadowedModule { pub fn new( module: Arc>, variables: Option<&HashSet>, functions: Option<&HashSet>, mixins: Option<&HashSet>, ) -> Self { let module_scope = module.borrow().scope(); let variables = Self::shadowed_map(Arc::clone(&module_scope.variables), variables); let functions = Self::shadowed_map(Arc::clone(&module_scope.functions), functions); let mixins = Self::shadowed_map(Arc::clone(&module_scope.mixins), mixins); let new_scope = ModuleScope { variables, functions, mixins, }; Self { inner: module, scope: new_scope, } } fn needs_blocklist( map: Arc>, blocklist: Option<&HashSet>, ) -> bool { blocklist.is_some() && !map.is_empty() && blocklist.unwrap().iter().any(|key| map.contains_key(*key)) } fn shadowed_map( map: Arc>, blocklist: Option<&HashSet>, ) -> Arc> { match blocklist { Some(..) if !Self::needs_blocklist(Arc::clone(&map), blocklist) => map, Some(blocklist) => Arc::new(LimitedMapView::blocklist(map, blocklist)), None => map, } } pub fn if_necessary( module: Arc>, variables: Option<&HashSet>, functions: Option<&HashSet>, mixins: Option<&HashSet>, ) -> Option>> { let module_scope = module.borrow().scope(); let needs_blocklist = Self::needs_blocklist(Arc::clone(&module_scope.variables), variables) || Self::needs_blocklist(Arc::clone(&module_scope.functions), functions) || Self::needs_blocklist(Arc::clone(&module_scope.mixins), mixins); if needs_blocklist { Some(Arc::new(RefCell::new(Module::Shadowed(Self::new( module, variables, functions, mixins, ))))) } else { None } } } #[derive(Debug, Clone)] pub(crate) struct ForwardedModule { scope: ModuleScope, #[allow(dead_code)] inner: Arc>, #[allow(dead_code)] forward_rule: AstForwardRule, } impl ForwardedModule { pub fn new(module: Arc>, rule: AstForwardRule) -> Self { let scope = (*module).borrow().scope(); let variables = Self::forwarded_map( scope.variables, rule.prefix.as_deref(), rule.shown_variables.as_ref(), rule.hidden_variables.as_ref(), ); let functions = Self::forwarded_map( scope.functions, rule.prefix.as_deref(), rule.shown_mixins_and_functions.as_ref(), rule.hidden_mixins_and_functions.as_ref(), ); let mixins = Self::forwarded_map( scope.mixins, rule.prefix.as_deref(), rule.shown_mixins_and_functions.as_ref(), rule.hidden_mixins_and_functions.as_ref(), ); let scope = ModuleScope { variables, mixins, functions, }; ForwardedModule { inner: module, forward_rule: rule, scope, } } fn forwarded_map( mut map: Arc>, prefix: Option<&str>, safelist: Option<&HashSet>, blocklist: Option<&HashSet>, ) -> Arc> { debug_assert!(safelist.is_none() || blocklist.is_none()); if prefix.is_none() && safelist.is_none() && blocklist.is_none() { return map; } if let Some(prefix) = prefix { map = Arc::new(PrefixedMapView(map, prefix.to_owned())); } map } pub fn if_necessary( module: Arc>, rule: AstForwardRule, ) -> Arc> { if rule.prefix.is_none() && rule.shown_mixins_and_functions.is_none() && rule.shown_variables.is_none() && rule .hidden_mixins_and_functions .as_ref() .map_or(false, HashSet::is_empty) && rule .hidden_variables .as_ref() .map_or(false, HashSet::is_empty) { module } else { Arc::new(RefCell::new(Module::Forwarded(ForwardedModule::new( module, rule, )))) } } } #[derive(Debug, Clone)] pub(crate) struct ModuleScope { pub variables: Arc>, pub mixins: Arc>, pub functions: Arc>, } impl ModuleScope { pub fn new() -> Self { Self { variables: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), mixins: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), functions: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), } } } #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum Module { Environment { scope: ModuleScope, #[allow(dead_code)] upstream: Vec, #[allow(dead_code)] extension_store: ExtensionStore, #[allow(dead_code)] env: Environment, }, Builtin { scope: ModuleScope, }, Forwarded(ForwardedModule), Shadowed(ShadowedModule), } #[derive(Debug, Clone)] pub(crate) struct Modules(pub BTreeMap>>); impl Modules { pub fn new() -> Self { Self(BTreeMap::new()) } pub fn insert( &mut self, name: Identifier, module: Arc>, span: Span, ) -> SassResult<()> { if self.0.contains_key(&name) { return Err(( format!("There's already a module with namespace \"{}\".", name), span, ) .into()); } self.0.insert(name, module); Ok(()) } pub fn get(&self, name: Identifier, span: Span) -> SassResult>> { match self.0.get(&name) { Some(v) => Ok(Arc::clone(v)), None => Err(( format!( "There is no module with the namespace \"{}\".", name.as_str() ), span, ) .into()), } } pub fn get_mut( &mut self, name: Identifier, span: Span, ) -> SassResult<&mut Arc>> { match self.0.get_mut(&name) { Some(v) => Ok(v), None => Err(( format!( "There is no module with the namespace \"{}\".", name.as_str() ), span, ) .into()), } } } fn member_map( local: Arc>, others: Vec>>, ) -> Arc> { let local_map = PublicMemberMapView(local); if others.is_empty() { return Arc::new(local_map); } let mut all_maps: Vec>> = others.into_iter().filter(|map| !map.is_empty()).collect(); all_maps.push(Arc::new(local_map)); // todo: potential optimization when all_maps.len() == 1 Arc::new(MergedMapView::new(all_maps)) } impl Module { pub fn new_env(env: Environment, extension_store: ExtensionStore) -> Self { let variables = { let variables = (*env.forwarded_modules).borrow(); let variables = variables .iter() .map(|module| Arc::clone(&(*module).borrow().scope().variables)); let this = Arc::new(BaseMapView(env.global_vars())); member_map(this, variables.collect()) }; let mixins = { let mixins = (*env.forwarded_modules).borrow(); let mixins = mixins .iter() .map(|module| Arc::clone(&(*module).borrow().scope().mixins)); let this = Arc::new(BaseMapView(env.global_mixins())); member_map(this, mixins.collect()) }; let functions = { let functions = (*env.forwarded_modules).borrow(); let functions = functions .iter() .map(|module| Arc::clone(&(*module).borrow().scope().functions)); let this = Arc::new(BaseMapView(env.global_functions())); member_map(this, functions.collect()) }; let scope = ModuleScope { variables, mixins, functions, }; Module::Environment { scope, upstream: Vec::new(), extension_store, env, } } pub fn new_builtin() -> Self { Module::Builtin { scope: ModuleScope::new(), } } pub(crate) fn scope(&self) -> ModuleScope { match self { Self::Builtin { scope } | Self::Environment { scope, .. } | Self::Forwarded(ForwardedModule { scope, .. }) | Self::Shadowed(ShadowedModule { scope, .. }) => scope.clone(), } } pub fn get_var(&self, name: Spanned) -> SassResult { let scope = self.scope(); match scope.variables.get(name.node) { Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), } } pub fn get_var_no_err(&self, name: Identifier) -> Option { let scope = self.scope(); scope.variables.get(name) } pub fn get_mixin_no_err(&self, name: Identifier) -> Option { let scope = self.scope(); scope.mixins.get(name) } pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { let scope = match self { Self::Builtin { .. } => { return Err(("Cannot modify built-in variable.", name.span).into()) } Self::Environment { scope, .. } | Self::Forwarded(ForwardedModule { scope, .. }) | Self::Shadowed(ShadowedModule { scope, .. }) => scope.clone(), }; if scope.variables.insert(name.node, value).is_none() { return Err(("Undefined variable.", name.span).into()); } Ok(()) } pub fn get_mixin(&self, name: Spanned) -> SassResult { let scope = self.scope(); match scope.mixins.get(name.node) { Some(v) => Ok(v), None => Err(("Undefined mixin.", name.span).into()), } } pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) { let scope = self.scope(); scope.mixins.insert(name.into(), Mixin::Builtin(mixin)); } pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) { let ident = name.into(); let scope = self.scope(); scope.variables.insert(ident, value); } pub fn get_fn(&self, name: Identifier) -> Option { let scope = self.scope(); scope.functions.get(name) } pub fn var_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.variables.get(name).is_some() } pub fn mixin_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.mixins.get(name).is_some() } pub fn fn_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.functions.get(name).is_some() } pub fn insert_builtin( &mut self, name: &'static str, function: fn(ArgumentResult, &mut Visitor) -> SassResult, ) { let ident = name.into(); let scope = match self { Self::Builtin { scope } => scope, _ => unreachable!(), }; scope .functions .insert(ident, SassFunction::Builtin(Builtin::new(function), ident)); } pub fn functions(&self, span: Span) -> SassMap { SassMap::new_with( self.scope() .functions .iter() .into_iter() .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted).span(span), Value::FunctionRef(Box::new(value)), ) }) .collect::>(), ) } pub fn variables(&self, span: Span) -> SassMap { SassMap::new_with( self.scope() .variables .iter() .into_iter() .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted).span(span), value, ) }) .collect::>(), ) } } pub(crate) fn declare_module_color() -> Module { let mut module = Module::new_builtin(); color::declare(&mut module); module } pub(crate) fn declare_module_list() -> Module { let mut module = Module::new_builtin(); list::declare(&mut module); module } pub(crate) fn declare_module_map() -> Module { let mut module = Module::new_builtin(); map::declare(&mut module); module } pub(crate) fn declare_module_math() -> Module { let mut module = Module::new_builtin(); math::declare(&mut module); module } pub(crate) fn declare_module_meta() -> Module { let mut module = Module::new_builtin(); meta::declare(&mut module); module } pub(crate) fn declare_module_selector() -> Module { let mut module = Module::new_builtin(); selector::declare(&mut module); module } pub(crate) fn declare_module_string() -> Module { let mut module = Module::new_builtin(); string::declare(&mut module); module } grass-0.13.4/crates/compiler/src/builtin/modules/selector.rs000066400000000000000000000012501465374720000241120ustar00rootroot00000000000000use crate::{ builtin::modules::Module, builtin::selector::{ is_superselector, selector_append, selector_extend, selector_nest, selector_parse, selector_replace, selector_unify, simple_selectors, }, }; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("is-superselector", is_superselector); f.insert_builtin("append", selector_append); f.insert_builtin("extend", selector_extend); f.insert_builtin("nest", selector_nest); f.insert_builtin("parse", selector_parse); f.insert_builtin("replace", selector_replace); f.insert_builtin("unify", selector_unify); f.insert_builtin("simple-selectors", simple_selectors); } grass-0.13.4/crates/compiler/src/builtin/modules/string.rs000066400000000000000000000014071465374720000236040ustar00rootroot00000000000000use crate::builtin::{ modules::Module, string::{ quote, str_index, str_insert, str_length, str_slice, str_split, to_lower_case, to_upper_case, unquote, }, }; #[cfg(feature = "random")] use crate::builtin::string::unique_id; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("quote", quote); f.insert_builtin("index", str_index); f.insert_builtin("insert", str_insert); f.insert_builtin("length", str_length); f.insert_builtin("slice", str_slice); f.insert_builtin("split", str_split); f.insert_builtin("to-lower-case", to_lower_case); f.insert_builtin("to-upper-case", to_upper_case); #[cfg(feature = "random")] f.insert_builtin("unique-id", unique_id); f.insert_builtin("unquote", unquote); } grass-0.13.4/crates/compiler/src/color/000077500000000000000000000000001465374720000177265ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/color/mod.rs000066400000000000000000000356051465374720000210640ustar00rootroot00000000000000//! A color is internally represented as either RGBA or HSLA. //! //! Colors can be constructed in Sass through names (e.g. red, blue, aqua) //! or the builtin functions `rgb()`, `rgba()`, `hsl()`, and `hsla()`, //! all of which can accept 1-4 arguments. //! //! It is necessary to retain the original values with which the //! color was constructed. //! E.g. `hsla(.999999999999, 100, 100, 1)` should retain its full HSLA //! values to an arbitrary precision. //! //! Color values matching named colors are implicitly converted to named colors //! E.g. `rgba(255, 0, 0, 1)` => `red` //! //! Named colors retain their original casing, //! so `rEd` should be emitted as `rEd`. use crate::value::{fuzzy_round, Number}; pub(crate) use name::NAMED_COLORS; mod name; // todo: only store alpha once on color #[derive(Debug, Clone)] pub struct Color { rgba: Rgb, hsla: Option, alpha: Number, pub(crate) 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 { fn eq(&self, other: &Self) -> bool { if self.alpha != other.alpha && !(self.alpha >= Number::one() && other.alpha >= Number::one()) { return false; } self.rgba == other.rgba } } impl Eq for Color {} impl Color { pub(crate) const fn new_rgba( red: Number, green: Number, blue: Number, alpha: Number, format: ColorFormat, ) -> Color { Color { rgba: Rgb::new(red, green, blue), alpha, hsla: None, format, } } const fn new_hsla(red: Number, green: Number, blue: Number, alpha: Number, hsla: Hsl) -> Color { Color { rgba: Rgb::new(red, green, blue), alpha, hsla: Some(hsla), format: ColorFormat::Infer, } } } #[derive(Debug, Clone)] struct Rgb { red: Number, green: Number, blue: Number, } impl PartialEq for Rgb { fn eq(&self, other: &Self) -> bool { if self.red != other.red && !(self.red >= Number(255.0) && other.red >= Number(255.0)) { return false; } if self.green != other.green && !(self.green >= Number(255.0) && other.green >= Number(255.0)) { return false; } if self.blue != other.blue && !(self.blue >= Number(255.0) && other.blue >= Number(255.0)) { return false; } true } } impl Eq for Rgb {} impl Rgb { pub const fn new(red: Number, green: Number, blue: Number) -> Self { Rgb { red, green, blue } } } #[derive(Debug, Clone)] struct Hsl { hue: Number, saturation: Number, luminance: Number, } impl Hsl { pub const fn new(hue: Number, saturation: Number, luminance: Number) -> Self { Hsl { hue, saturation, luminance, } } pub fn hue(&self) -> Number { self.hue } pub fn saturation(&self) -> Number { self.saturation } pub fn luminance(&self) -> Number { self.luminance } } // RGBA color functions impl Color { pub fn new(red: u8, green: u8, blue: u8, alpha: u8, format: String) -> Self { Color { rgba: Rgb::new(red.into(), green.into(), blue.into()), hsla: None, alpha: alpha.into(), format: ColorFormat::Literal(format), } } /// Create a new `Color` with just RGBA values. /// Color representation is created automatically. pub fn from_rgba( 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::Infer) } pub fn from_rgba_fn( mut red: Number, mut green: Number, mut blue: Number, mut alpha: Number, ) -> Self { red = red.clamp(0.0, 255.0); green = green.clamp(0.0, 255.0); blue = blue.clamp(0.0, 255.0); alpha = alpha.clamp(0.0, 1.0); Color::new_rgba(red, green, blue, alpha, ColorFormat::Rgb) } pub fn red(&self) -> Number { self.rgba.red.round() } pub fn blue(&self) -> Number { self.rgba.blue.round() } pub fn green(&self) -> Number { self.rgba.green.round() } /// Mix two colors together with weight /// Algorithm adapted from /// pub fn mix(&self, other: &Color, weight: Number) -> Self { let weight = weight.clamp(0.0, 100.0); let normalized_weight = weight * Number(2.0) - Number::one(); let alpha_distance = self.alpha() - other.alpha(); let combined_weight1 = if normalized_weight * alpha_distance == Number(-1.0) { normalized_weight } else { (normalized_weight + alpha_distance) / (Number::one() + normalized_weight * alpha_distance) }; let weight1 = (combined_weight1 + Number::one()) / Number(2.0); let weight2 = Number::one() - weight1; Color::from_rgba( self.red() * weight1 + other.red() * weight2, self.green() * weight1 + other.green() * weight2, self.blue() * weight1 + other.blue() * weight2, self.alpha() * weight + other.alpha() * (Number::one() - weight), ) } } /// HSLA color functions /// Algorithms adapted from impl Color { /// Calculate hue from RGBA values pub fn hue(&self) -> Number { if let Some(h) = &self.hsla { return h.hue(); } let red = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); let delta = max - min; let hue = if min == max { Number::zero() } else if max == red { Number(60.0) * (green - blue) / delta } else if max == green { Number(120.0) + Number(60.0) * (blue - red) / delta } else { Number(240.0) + Number(60.0) * (red - green) / delta }; hue % Number(360.0) } /// Calculate saturation from RGBA values pub fn saturation(&self) -> Number { if let Some(h) = &self.hsla { return h.saturation() * Number(100.0); } let red: Number = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); if min == max { return Number::zero(); } let delta = max - min; let sum = max + min; let s = delta / if sum > Number::one() { Number(2.0) - sum } else { sum }; s * Number(100.0) } /// Calculate luminance from RGBA values pub fn lightness(&self) -> Number { if let Some(h) = &self.hsla { return h.luminance() * Number(100.0); } let red: Number = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); (((min + max) / Number(2.0)) * Number(100.0)).round() } pub fn as_hsla(&self) -> (Number, Number, Number, Number) { if let Some(h) = &self.hsla { return (h.hue(), h.saturation(), h.luminance(), self.alpha()); } let red = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); let lightness = (min + max) / Number(2.0); let saturation = if min == max { Number::zero() } else { let d = max - min; let mm = max + min; d / if mm > Number::one() { Number(2.0) - mm } else { mm } }; let mut hue = if min == max { Number::zero() } else if blue == max { Number(4.0) + (red - green) / (max - min) } else if green == max { Number(2.0) + (blue - red) / (max - min) } else { (green - blue) / (max - min) }; if hue.is_negative() { hue += Number(360.0); } hue *= Number(60.0); (hue % Number(360.0), saturation, lightness, self.alpha()) } pub fn adjust_hue(&self, degrees: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue + degrees, saturation, luminance, alpha) } pub fn lighten(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, saturation, luminance + amount, alpha) } pub fn darken(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, saturation, luminance - amount, alpha) } pub fn saturate(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, (saturation + amount).clamp(0.0, 1.0), luminance, alpha) } pub fn desaturate(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, (saturation - amount).clamp(0.0, 1.0), luminance, alpha) } pub fn from_hsla_fn(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self { let mut color = Self::from_hsla(hue, saturation, luminance, alpha); color.format = ColorFormat::Hsl; color } /// Create RGBA representation from HSLA values pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self { let hue = hue % Number(360.0); let hsla = Hsl::new(hue, saturation.clamp(0.0, 1.0), lightness.clamp(0.0, 1.0)); let scaled_hue = hue.0 / 360.0; let scaled_saturation = saturation.0.clamp(0.0, 1.0); let scaled_lightness = lightness.0.clamp(0.0, 1.0); let m2 = if scaled_lightness <= 0.5 { scaled_lightness * (scaled_saturation + 1.0) } else { scaled_lightness.mul_add(-scaled_saturation, scaled_lightness + scaled_saturation) }; let m1 = scaled_lightness.mul_add(2.0, -m2); let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0); let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0); let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0); Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla) } fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 { if hue < 0.0 { hue += 1.0; } if hue > 1.0 { hue -= 1.0; } if hue < 1.0 / 6.0 { ((m2 - m1) * hue).mul_add(6.0, m1) } else if hue < 1.0 / 2.0 { m2 } else if hue < 2.0 / 3.0 { ((m2 - m1) * (2.0 / 3.0 - hue)).mul_add(6.0, m1) } else { m1 } } pub fn invert(&self, weight: Number) -> Self { if weight.is_zero() { return self.clone(); } let red = Number(255.0) - self.red(); let green = Number(255.0) - self.green(); let blue = Number(255.0) - self.blue(); let inverse = Color::new_rgba(red, green, blue, self.alpha(), ColorFormat::Infer); inverse.mix(self, weight) } pub fn complement(&self) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue + Number(180.0), saturation, luminance, alpha) } } /// Opacity color functions impl Color { pub fn alpha(&self) -> Number { if self.alpha > Number::one() { self.alpha / Number(255.0) } else { self.alpha } } /// Change `alpha` to value given pub fn with_alpha(&self, alpha: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), alpha) } /// Makes a color more opaque. /// Takes a color and a number between 0 and 1, /// and returns a color with the opacity increased by that amount. pub fn fade_in(&self, amount: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), self.alpha() + amount) } /// Makes a color more transparent. /// Takes a color and a number between 0 and 1, /// and returns a color with the opacity decreased by that amount. pub fn fade_out(&self, amount: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), self.alpha() - amount) } } /// Other color functions impl Color { pub fn to_ie_hex_str(&self) -> String { format!( "#{:02X}{:02X}{:02X}{:02X}", fuzzy_round(self.alpha().0 * 255.0) as u8, self.red().0 as u8, self.green().0 as u8, self.blue().0 as u8 ) } } /// HWB color functions impl Color { pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color { let hue = Number(hue.rem_euclid(360.0) / 360.0); let mut scaled_white = white.0 / 100.0; let mut scaled_black = black.0 / 100.0; alpha = alpha.clamp(0.0, 1.0); let white_black_sum = scaled_white + scaled_black; if white_black_sum > 1.0 { scaled_white /= white_black_sum; scaled_black /= white_black_sum; } let factor = 1.0 - scaled_white - scaled_black; let to_rgb = |hue: f64| -> Number { let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white); Number(fuzzy_round(channel * 255.0)) }; let red = to_rgb(hue.0 + 1.0 / 3.0); let green = to_rgb(hue.0); let blue = to_rgb(hue.0 - 1.0 / 3.0); Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer) } pub fn whiteness(&self) -> Number { self.red().min(self.green()).min(self.blue()) / Number(255.0) } pub fn blackness(&self) -> Number { Number(1.0) - (self.red().max(self.green()).max(self.blue()) / Number(255.0)) } } grass-0.13.4/crates/compiler/src/color/name.rs000066400000000000000000000330521465374720000212170ustar00rootroot00000000000000//! A big dictionary of named colors and their corresponding RGBA values pub(crate) struct NamedColorMap { name_to_rgba: phf::Map<&'static str, [u8; 4]>, rgba_to_name: phf::Map<[u8; 3], &'static str>, } impl NamedColorMap { pub fn get_by_name(&self, name: &str) -> Option<&[u8; 4]> { self.name_to_rgba.get(name) } pub fn get_by_rgba(&self, rgba: [u8; 3]) -> Option<&&str> { self.rgba_to_name.get(&rgba) } } pub(crate) static NAMED_COLORS: NamedColorMap = NamedColorMap { name_to_rgba: phf::phf_map! { "aliceblue" => [0xF0, 0xF8, 0xFF, 0xFF], "antiquewhite" => [0xFA, 0xEB, 0xD7, 0xFF], "aqua" => [0x00, 0xFF, 0xFF, 0xFF], "cyan" => [0x00, 0xFF, 0xFF, 0xFF], "aquamarine" => [0x7F, 0xFF, 0xD4, 0xFF], "azure" => [0xF0, 0xFF, 0xFF, 0xFF], "beige" => [0xF5, 0xF5, 0xDC, 0xFF], "bisque" => [0xFF, 0xE4, 0xC4, 0xFF], "black" => [0x00, 0x00, 0x00, 0xFF], "blanchedalmond" => [0xFF, 0xEB, 0xCD, 0xFF], "blue" => [0x00, 0x00, 0xFF, 0xFF], "blueviolet" => [0x8A, 0x2B, 0xE2, 0xFF], "brown" => [0xA5, 0x2A, 0x2A, 0xFF], "burlywood" => [0xDE, 0xB8, 0x87, 0xFF], "cadetblue" => [0x5F, 0x9E, 0xA0, 0xFF], "chartreuse" => [0x7F, 0xFF, 0x00, 0xFF], "chocolate" => [0xD2, 0x69, 0x1E, 0xFF], "coral" => [0xFF, 0x7F, 0x50, 0xFF], "cornflowerblue" => [0x64, 0x95, 0xED, 0xFF], "cornsilk" => [0xFF, 0xF8, 0xDC, 0xFF], "crimson" => [0xDC, 0x14, 0x3C, 0xFF], "darkblue" => [0x00, 0x00, 0x8B, 0xFF], "darkcyan" => [0x00, 0x8B, 0x8B, 0xFF], "darkgoldenrod" => [0xB8, 0x86, 0x0B, 0xFF], "darkgray" => [0xA9, 0xA9, 0xA9, 0xFF], "darkgrey" => [0xA9, 0xA9, 0xA9, 0xFF], "darkgreen" => [0x00, 0x64, 0x00, 0xFF], "darkkhaki" => [0xBD, 0xB7, 0x6B, 0xFF], "darkmagenta" => [0x8B, 0x00, 0x8B, 0xFF], "darkolivegreen" => [0x55, 0x6B, 0x2F, 0xFF], "darkorange" => [0xFF, 0x8C, 0x00, 0xFF], "darkorchid" => [0x99, 0x32, 0xCC, 0xFF], "darkred" => [0x8B, 0x00, 0x00, 0xFF], "darksalmon" => [0xE9, 0x96, 0x7A, 0xFF], "darkseagreen" => [0x8F, 0xBC, 0x8F, 0xFF], "darkslateblue" => [0x48, 0x3D, 0x8B, 0xFF], "darkslategray" => [0x2F, 0x4F, 0x4F, 0xFF], "darkslategrey" => [0x2F, 0x4F, 0x4F, 0xFF], "darkturquoise" => [0x00, 0xCE, 0xD1, 0xFF], "darkviolet" => [0x94, 0x00, 0xD3, 0xFF], "deeppink" => [0xFF, 0x14, 0x93, 0xFF], "deepskyblue" => [0x00, 0xBF, 0xFF, 0xFF], "dimgray" => [0x69, 0x69, 0x69, 0xFF], "dimgrey" => [0x69, 0x69, 0x69, 0xFF], "dodgerblue" => [0x1E, 0x90, 0xFF, 0xFF], "firebrick" => [0xB2, 0x22, 0x22, 0xFF], "floralwhite" => [0xFF, 0xFA, 0xF0, 0xFF], "forestgreen" => [0x22, 0x8B, 0x22, 0xFF], "fuchsia" => [0xFF, 0x00, 0xFF, 0xFF], "magenta" => [0xFF, 0x00, 0xFF, 0xFF], "gainsboro" => [0xDC, 0xDC, 0xDC, 0xFF], "ghostwhite" => [0xF8, 0xF8, 0xFF, 0xFF], "gold" => [0xFF, 0xD7, 0x00, 0xFF], "goldenrod" => [0xDA, 0xA5, 0x20, 0xFF], "gray" => [0x80, 0x80, 0x80, 0xFF], "grey" => [0x80, 0x80, 0x80, 0xFF], "green" => [0x00, 0x80, 0x00, 0xFF], "greenyellow" => [0xAD, 0xFF, 0x2F, 0xFF], "honeydew" => [0xF0, 0xFF, 0xF0, 0xFF], "hotpink" => [0xFF, 0x69, 0xB4, 0xFF], "indianred" => [0xCD, 0x5C, 0x5C, 0xFF], "indigo" => [0x4B, 0x00, 0x82, 0xFF], "ivory" => [0xFF, 0xFF, 0xF0, 0xFF], "khaki" => [0xF0, 0xE6, 0x8C, 0xFF], "lavender" => [0xE6, 0xE6, 0xFA, 0xFF], "lavenderblush" => [0xFF, 0xF0, 0xF5, 0xFF], "lawngreen" => [0x7C, 0xFC, 0x00, 0xFF], "lemonchiffon" => [0xFF, 0xFA, 0xCD, 0xFF], "lightblue" => [0xAD, 0xD8, 0xE6, 0xFF], "lightcoral" => [0xF0, 0x80, 0x80, 0xFF], "lightcyan" => [0xE0, 0xFF, 0xFF, 0xFF], "lightgoldenrodyellow" => [0xFA, 0xFA, 0xD2, 0xFF], "lightgray" => [0xD3, 0xD3, 0xD3, 0xFF], "lightgrey" => [0xD3, 0xD3, 0xD3, 0xFF], "lightgreen" => [0x90, 0xEE, 0x90, 0xFF], "lightpink" => [0xFF, 0xB6, 0xC1, 0xFF], "lightsalmon" => [0xFF, 0xA0, 0x7A, 0xFF], "lightseagreen" => [0x20, 0xB2, 0xAA, 0xFF], "lightskyblue" => [0x87, 0xCE, 0xFA, 0xFF], "lightslategray" => [0x77, 0x88, 0x99, 0xFF], "lightslategrey" => [0x77, 0x88, 0x99, 0xFF], "lightsteelblue" => [0xB0, 0xC4, 0xDE, 0xFF], "lightyellow" => [0xFF, 0xFF, 0xE0, 0xFF], "lime" => [0x00, 0xFF, 0x00, 0xFF], "limegreen" => [0x32, 0xCD, 0x32, 0xFF], "linen" => [0xFA, 0xF0, 0xE6, 0xFF], "maroon" => [0x80, 0x00, 0x00, 0xFF], "mediumaquamarine" => [0x66, 0xCD, 0xAA, 0xFF], "mediumblue" => [0x00, 0x00, 0xCD, 0xFF], "mediumorchid" => [0xBA, 0x55, 0xD3, 0xFF], "mediumpurple" => [0x93, 0x70, 0xDB, 0xFF], "mediumseagreen" => [0x3C, 0xB3, 0x71, 0xFF], "mediumslateblue" => [0x7B, 0x68, 0xEE, 0xFF], "mediumspringgreen" => [0x00, 0xFA, 0x9A, 0xFF], "mediumturquoise" => [0x48, 0xD1, 0xCC, 0xFF], "mediumvioletred" => [0xC7, 0x15, 0x85, 0xFF], "midnightblue" => [0x19, 0x19, 0x70, 0xFF], "mintcream" => [0xF5, 0xFF, 0xFA, 0xFF], "mistyrose" => [0xFF, 0xE4, 0xE1, 0xFF], "moccasin" => [0xFF, 0xE4, 0xB5, 0xFF], "navajowhite" => [0xFF, 0xDE, 0xAD, 0xFF], "navy" => [0x00, 0x00, 0x80, 0xFF], "oldlace" => [0xFD, 0xF5, 0xE6, 0xFF], "olive" => [0x80, 0x80, 0x00, 0xFF], "olivedrab" => [0x6B, 0x8E, 0x23, 0xFF], "orange" => [0xFF, 0xA5, 0x00, 0xFF], "orangered" => [0xFF, 0x45, 0x00, 0xFF], "orchid" => [0xDA, 0x70, 0xD6, 0xFF], "palegoldenrod" => [0xEE, 0xE8, 0xAA, 0xFF], "palegreen" => [0x98, 0xFB, 0x98, 0xFF], "paleturquoise" => [0xAF, 0xEE, 0xEE, 0xFF], "palevioletred" => [0xDB, 0x70, 0x93, 0xFF], "papayawhip" => [0xFF, 0xEF, 0xD5, 0xFF], "peachpuff" => [0xFF, 0xDA, 0xB9, 0xFF], "peru" => [0xCD, 0x85, 0x3F, 0xFF], "pink" => [0xFF, 0xC0, 0xCB, 0xFF], "plum" => [0xDD, 0xA0, 0xDD, 0xFF], "powderblue" => [0xB0, 0xE0, 0xE6, 0xFF], "purple" => [0x80, 0x00, 0x80, 0xFF], "rebeccapurple" => [0x66, 0x33, 0x99, 0xFF], "red" => [0xFF, 0x00, 0x00, 0xFF], "rosybrown" => [0xBC, 0x8F, 0x8F, 0xFF], "royalblue" => [0x41, 0x69, 0xE1, 0xFF], "saddlebrown" => [0x8B, 0x45, 0x13, 0xFF], "salmon" => [0xFA, 0x80, 0x72, 0xFF], "sandybrown" => [0xF4, 0xA4, 0x60, 0xFF], "seagreen" => [0x2E, 0x8B, 0x57, 0xFF], "seashell" => [0xFF, 0xF5, 0xEE, 0xFF], "sienna" => [0xA0, 0x52, 0x2D, 0xFF], "silver" => [0xC0, 0xC0, 0xC0, 0xFF], "skyblue" => [0x87, 0xCE, 0xEB, 0xFF], "slateblue" => [0x6A, 0x5A, 0xCD, 0xFF], "slategray" => [0x70, 0x80, 0x90, 0xFF], "slategrey" => [0x70, 0x80, 0x90, 0xFF], "snow" => [0xFF, 0xFA, 0xFA, 0xFF], "springgreen" => [0x00, 0xFF, 0x7F, 0xFF], "steelblue" => [0x46, 0x82, 0xB4, 0xFF], "tan" => [0xD2, 0xB4, 0x8C, 0xFF], "teal" => [0x00, 0x80, 0x80, 0xFF], "thistle" => [0xD8, 0xBF, 0xD8, 0xFF], "tomato" => [0xFF, 0x63, 0x47, 0xFF], "turquoise" => [0x40, 0xE0, 0xD0, 0xFF], "violet" => [0xEE, 0x82, 0xEE, 0xFF], "wheat" => [0xF5, 0xDE, 0xB3, 0xFF], "white" => [0xFF, 0xFF, 0xFF, 0xFF], "whitesmoke" => [0xF5, 0xF5, 0xF5, 0xFF], "yellow" => [0xFF, 0xFF, 0x00, 0xFF], "yellowgreen" => [0x9A, 0xCD, 0x32, 0xFF], "transparent" => [0x00, 0x00, 0x00, 0x00], }, rgba_to_name: phf::phf_map! { [0xF0, 0xF8, 0xFF] => "aliceblue", [0xFA, 0xEB, 0xD7] => "antiquewhite", [0x00, 0xFF, 0xFF] => "aqua", [0x7F, 0xFF, 0xD4] => "aquamarine", [0xF0, 0xFF, 0xFF] => "azure", [0xF5, 0xF5, 0xDC] => "beige", [0xFF, 0xE4, 0xC4] => "bisque", [0x00, 0x00, 0x00] => "black", [0xFF, 0xEB, 0xCD] => "blanchedalmond", [0x00, 0x00, 0xFF] => "blue", [0x8A, 0x2B, 0xE2] => "blueviolet", [0xA5, 0x2A, 0x2A] => "brown", [0xDE, 0xB8, 0x87] => "burlywood", [0x5F, 0x9E, 0xA0] => "cadetblue", [0x7F, 0xFF, 0x00] => "chartreuse", [0xD2, 0x69, 0x1E] => "chocolate", [0xFF, 0x7F, 0x50] => "coral", [0x64, 0x95, 0xED] => "cornflowerblue", [0xFF, 0xF8, 0xDC] => "cornsilk", [0xDC, 0x14, 0x3C] => "crimson", [0x00, 0x00, 0x8B] => "darkblue", [0x00, 0x8B, 0x8B] => "darkcyan", [0xB8, 0x86, 0x0B] => "darkgoldenrod", [0xA9, 0xA9, 0xA9] => "darkgray", [0x00, 0x64, 0x00] => "darkgreen", [0xBD, 0xB7, 0x6B] => "darkkhaki", [0x8B, 0x00, 0x8B] => "darkmagenta", [0x55, 0x6B, 0x2F] => "darkolivegreen", [0xFF, 0x8C, 0x00] => "darkorange", [0x99, 0x32, 0xCC] => "darkorchid", [0x8B, 0x00, 0x00] => "darkred", [0xE9, 0x96, 0x7A] => "darksalmon", [0x8F, 0xBC, 0x8F] => "darkseagreen", [0x48, 0x3D, 0x8B] => "darkslateblue", [0x2F, 0x4F, 0x4F] => "darkslategray", [0x00, 0xCE, 0xD1] => "darkturquoise", [0x94, 0x00, 0xD3] => "darkviolet", [0xFF, 0x14, 0x93] => "deeppink", [0x00, 0xBF, 0xFF] => "deepskyblue", [0x69, 0x69, 0x69] => "dimgray", [0x1E, 0x90, 0xFF] => "dodgerblue", [0xB2, 0x22, 0x22] => "firebrick", [0xFF, 0xFA, 0xF0] => "floralwhite", [0x22, 0x8B, 0x22] => "forestgreen", [0xFF, 0x00, 0xFF] => "fuchsia", [0xDC, 0xDC, 0xDC] => "gainsboro", [0xF8, 0xF8, 0xFF] => "ghostwhite", [0xFF, 0xD7, 0x00] => "gold", [0xDA, 0xA5, 0x20] => "goldenrod", [0x80, 0x80, 0x80] => "gray", [0x00, 0x80, 0x00] => "green", [0xAD, 0xFF, 0x2F] => "greenyellow", [0xF0, 0xFF, 0xF0] => "honeydew", [0xFF, 0x69, 0xB4] => "hotpink", [0xCD, 0x5C, 0x5C] => "indianred", [0x4B, 0x00, 0x82] => "indigo", [0xFF, 0xFF, 0xF0] => "ivory", [0xF0, 0xE6, 0x8C] => "khaki", [0xE6, 0xE6, 0xFA] => "lavender", [0xFF, 0xF0, 0xF5] => "lavenderblush", [0x7C, 0xFC, 0x00] => "lawngreen", [0xFF, 0xFA, 0xCD] => "lemonchiffon", [0xAD, 0xD8, 0xE6] => "lightblue", [0xF0, 0x80, 0x80] => "lightcoral", [0xE0, 0xFF, 0xFF] => "lightcyan", [0xFA, 0xFA, 0xD2] => "lightgoldenrodyellow", [0xD3, 0xD3, 0xD3] => "lightgray", [0x90, 0xEE, 0x90] => "lightgreen", [0xFF, 0xB6, 0xC1] => "lightpink", [0xFF, 0xA0, 0x7A] => "lightsalmon", [0x20, 0xB2, 0xAA] => "lightseagreen", [0x87, 0xCE, 0xFA] => "lightskyblue", [0x77, 0x88, 0x99] => "lightslategray", [0xB0, 0xC4, 0xDE] => "lightsteelblue", [0xFF, 0xFF, 0xE0] => "lightyellow", [0x00, 0xFF, 0x00] => "lime", [0x32, 0xCD, 0x32] => "limegreen", [0xFA, 0xF0, 0xE6] => "linen", [0x80, 0x00, 0x00] => "maroon", [0x66, 0xCD, 0xAA] => "mediumaquamarine", [0x00, 0x00, 0xCD] => "mediumblue", [0xBA, 0x55, 0xD3] => "mediumorchid", [0x93, 0x70, 0xDB] => "mediumpurple", [0x3C, 0xB3, 0x71] => "mediumseagreen", [0x7B, 0x68, 0xEE] => "mediumslateblue", [0x00, 0xFA, 0x9A] => "mediumspringgreen", [0x48, 0xD1, 0xCC] => "mediumturquoise", [0xC7, 0x15, 0x85] => "mediumvioletred", [0x19, 0x19, 0x70] => "midnightblue", [0xF5, 0xFF, 0xFA] => "mintcream", [0xFF, 0xE4, 0xE1] => "mistyrose", [0xFF, 0xE4, 0xB5] => "moccasin", [0xFF, 0xDE, 0xAD] => "navajowhite", [0x00, 0x00, 0x80] => "navy", [0xFD, 0xF5, 0xE6] => "oldlace", [0x80, 0x80, 0x00] => "olive", [0x6B, 0x8E, 0x23] => "olivedrab", [0xFF, 0xA5, 0x00] => "orange", [0xFF, 0x45, 0x00] => "orangered", [0xDA, 0x70, 0xD6] => "orchid", [0xEE, 0xE8, 0xAA] => "palegoldenrod", [0x98, 0xFB, 0x98] => "palegreen", [0xAF, 0xEE, 0xEE] => "paleturquoise", [0xDB, 0x70, 0x93] => "palevioletred", [0xFF, 0xEF, 0xD5] => "papayawhip", [0xFF, 0xDA, 0xB9] => "peachpuff", [0xCD, 0x85, 0x3F] => "peru", [0xFF, 0xC0, 0xCB] => "pink", [0xDD, 0xA0, 0xDD] => "plum", [0xB0, 0xE0, 0xE6] => "powderblue", [0x80, 0x00, 0x80] => "purple", [0x66, 0x33, 0x99] => "rebeccapurple", [0xFF, 0x00, 0x00] => "red", [0xBC, 0x8F, 0x8F] => "rosybrown", [0x41, 0x69, 0xE1] => "royalblue", [0x8B, 0x45, 0x13] => "saddlebrown", [0xFA, 0x80, 0x72] => "salmon", [0xF4, 0xA4, 0x60] => "sandybrown", [0x2E, 0x8B, 0x57] => "seagreen", [0xFF, 0xF5, 0xEE] => "seashell", [0xA0, 0x52, 0x2D] => "sienna", [0xC0, 0xC0, 0xC0] => "silver", [0x87, 0xCE, 0xEB] => "skyblue", [0x6A, 0x5A, 0xCD] => "slateblue", [0x70, 0x80, 0x90] => "slategray", [0xFF, 0xFA, 0xFA] => "snow", [0x00, 0xFF, 0x7F] => "springgreen", [0x46, 0x82, 0xB4] => "steelblue", [0xD2, 0xB4, 0x8C] => "tan", [0x00, 0x80, 0x80] => "teal", [0xD8, 0xBF, 0xD8] => "thistle", [0xFF, 0x63, 0x47] => "tomato", [0x40, 0xE0, 0xD0] => "turquoise", [0xEE, 0x82, 0xEE] => "violet", [0xF5, 0xDE, 0xB3] => "wheat", [0xFF, 0xFF, 0xFF] => "white", [0xF5, 0xF5, 0xF5] => "whitesmoke", [0xFF, 0xFF, 0x00] => "yellow", [0x9A, 0xCD, 0x32] => "yellowgreen", }, }; grass-0.13.4/crates/compiler/src/common.rs000066400000000000000000000107101465374720000204450ustar00rootroot00000000000000use std::fmt::{self, Display, Write}; use crate::interner::InternedString; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum UnaryOp { Plus, Neg, Div, Not, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum BinaryOp { SingleEq, Equal, NotEqual, GreaterThan, GreaterThanEqual, LessThan, LessThanEqual, Plus, Minus, Mul, Div, Rem, And, Or, } impl BinaryOp { pub fn precedence(self) -> u8 { match self { Self::SingleEq => 0, Self::Or => 1, Self::And => 2, Self::Equal | Self::NotEqual => 3, Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan | Self::LessThanEqual => 4, Self::Plus | Self::Minus => 5, Self::Mul | Self::Div | Self::Rem => 6, } } } impl Display for BinaryOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BinaryOp::SingleEq => write!(f, "="), BinaryOp::Equal => write!(f, "=="), BinaryOp::NotEqual => write!(f, "!="), BinaryOp::GreaterThanEqual => write!(f, ">="), BinaryOp::LessThanEqual => write!(f, "<="), BinaryOp::GreaterThan => write!(f, ">"), BinaryOp::LessThan => write!(f, "<"), BinaryOp::Plus => write!(f, "+"), BinaryOp::Minus => write!(f, "-"), BinaryOp::Mul => write!(f, "*"), BinaryOp::Div => write!(f, "/"), BinaryOp::Rem => write!(f, "%"), BinaryOp::And => write!(f, "and"), BinaryOp::Or => write!(f, "or"), } } } /// Strings can either have quotes or not #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum QuoteKind { Quoted, None, } impl Display for QuoteKind { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Quoted => f.write_char('"'), Self::None => Ok(()), } } } /// Lists can either be bracketed or not #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Brackets { None, Bracketed, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ListSeparator { Space, Comma, Slash, Undecided, } impl ListSeparator { pub fn as_str(self) -> &'static str { match self { Self::Space | Self::Undecided => " ", Self::Comma => ", ", Self::Slash => " / ", } } pub fn name(self) -> &'static str { match self { Self::Space | Self::Undecided => "space", Self::Comma => "comma", Self::Slash => "slash", } } } /// In Sass, underscores and hyphens are considered equal when inside identifiers. /// /// This struct protects that invariant by normalizing all underscores into hyphens. #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)] pub struct Identifier(InternedString); impl fmt::Debug for Identifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Identifier") .field(&self.0.to_string()) .finish() } } impl Identifier { fn from_str(s: &str) -> Self { if s.contains('_') { Identifier(InternedString::get_or_intern(s.replace('_', "-"))) } else { Identifier(InternedString::get_or_intern(s)) } } pub fn is_public(&self) -> bool { !self.as_str().starts_with('-') } } impl From for Identifier { fn from(s: String) -> Identifier { Self::from_str(&s) } } impl From<&String> for Identifier { fn from(s: &String) -> Identifier { Self::from_str(s) } } impl From<&str> for Identifier { fn from(s: &str) -> Identifier { Self::from_str(s) } } impl Display for Identifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl Identifier { pub fn as_str(&self) -> &str { self.0.resolve_ref() } } /// Returns `name` without a vendor prefix. /// /// If `name` has no vendor prefix, it's returned as-is. pub(crate) fn unvendor(name: &str) -> &str { let bytes = name.as_bytes(); if bytes.len() < 2 { return name; } if bytes.first() != Some(&b'-') || bytes.get(1_usize) == Some(&b'-') { return name; } for i in 2..bytes.len() { if bytes.get(i) == Some(&b'-') { return &name[i + 1..]; } } name } grass-0.13.4/crates/compiler/src/context_flags.rs000066400000000000000000000060331465374720000220200ustar00rootroot00000000000000use std::ops::{BitAnd, BitOr, BitOrAssign}; #[derive(Debug, Copy, Clone)] pub(crate) struct ContextFlags(pub u16); #[derive(Debug, Copy, Clone)] pub(crate) struct ContextFlag(u16); impl ContextFlags { pub const IN_MIXIN: ContextFlag = ContextFlag(1); pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1); pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2); pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3); pub const FOUND_CONTENT_RULE: ContextFlag = ContextFlag(1 << 4); pub const IN_STYLE_RULE: ContextFlag = ContextFlag(1 << 5); pub const IN_UNKNOWN_AT_RULE: ContextFlag = ContextFlag(1 << 6); pub const IN_CONTENT_BLOCK: ContextFlag = ContextFlag(1 << 7); pub const IS_USE_ALLOWED: ContextFlag = ContextFlag(1 << 8); pub const IN_PARENS: ContextFlag = ContextFlag(1 << 9); pub const AT_ROOT_EXCLUDING_STYLE_RULE: ContextFlag = ContextFlag(1 << 10); pub const IN_SUPPORTS_DECLARATION: ContextFlag = ContextFlag(1 << 11); pub const IN_SEMI_GLOBAL_SCOPE: ContextFlag = ContextFlag(1 << 12); pub const fn empty() -> Self { Self(0) } pub fn unset(&mut self, flag: ContextFlag) { self.0 &= !flag.0; } pub fn set(&mut self, flag: ContextFlag, v: bool) { if v { self.0 |= flag.0; } else { self.unset(flag); } } pub fn in_mixin(self) -> bool { (self.0 & Self::IN_MIXIN) != 0 } pub fn in_function(self) -> bool { (self.0 & Self::IN_FUNCTION) != 0 } pub fn in_control_flow(self) -> bool { (self.0 & Self::IN_CONTROL_FLOW) != 0 } pub fn in_keyframes(self) -> bool { (self.0 & Self::IN_KEYFRAMES) != 0 } pub fn in_style_rule(self) -> bool { (self.0 & Self::IN_STYLE_RULE) != 0 } pub fn in_unknown_at_rule(self) -> bool { (self.0 & Self::IN_UNKNOWN_AT_RULE) != 0 } pub fn in_content_block(self) -> bool { (self.0 & Self::IN_CONTENT_BLOCK) != 0 } pub fn in_parens(self) -> bool { (self.0 & Self::IN_PARENS) != 0 } pub fn at_root_excluding_style_rule(self) -> bool { (self.0 & Self::AT_ROOT_EXCLUDING_STYLE_RULE) != 0 } pub fn in_supports_declaration(self) -> bool { (self.0 & Self::IN_SUPPORTS_DECLARATION) != 0 } pub fn in_semi_global_scope(self) -> bool { (self.0 & Self::IN_SEMI_GLOBAL_SCOPE) != 0 } pub fn found_content_rule(self) -> bool { (self.0 & Self::FOUND_CONTENT_RULE) != 0 } pub fn is_use_allowed(self) -> bool { (self.0 & Self::IS_USE_ALLOWED) != 0 } } impl BitAnd for u16 { type Output = Self; #[inline] fn bitand(self, rhs: ContextFlag) -> Self::Output { self & rhs.0 } } impl BitOr for ContextFlags { type Output = Self; fn bitor(self, rhs: ContextFlag) -> Self::Output { Self(self.0 | rhs.0) } } impl BitOrAssign for ContextFlags { fn bitor_assign(&mut self, rhs: ContextFlag) { self.0 |= rhs.0; } } grass-0.13.4/crates/compiler/src/error.rs000066400000000000000000000145401465374720000203130ustar00rootroot00000000000000use std::{ error::Error, fmt::{self, Display}, io, string::FromUtf8Error, sync::Arc, }; use codemap::{Span, SpanLoc}; pub type SassResult = Result>; /// `SassError`s can be either a structured error specific to `grass` or an /// `io::Error`. /// /// In the former case, the best way to interact with the error is to simply print /// it to the user. The `Display` implementation of this kind of error mirrors /// that of the errors `dart-sass` emits, e.g. ///```scss /// Error: $number: foo is not a number. /// | /// 308 | color: unit(foo); /// | ^^^ /// | /// ./input.scss:308:17 ///``` /// #[derive(Debug, Clone)] pub struct SassError { kind: SassErrorKind, } impl SassError { #[must_use] pub fn kind(self) -> PublicSassErrorKind { match self.kind { SassErrorKind::ParseError { message, loc, unicode, } => PublicSassErrorKind::ParseError { message, loc, unicode, }, SassErrorKind::FromUtf8Error(s) => PublicSassErrorKind::FromUtf8Error(s), SassErrorKind::IoError(io) => PublicSassErrorKind::IoError(io), SassErrorKind::Raw(..) => unreachable!("raw errors should not be accessible by users"), } } pub(crate) fn raw(self) -> (String, Span) { match self.kind { SassErrorKind::Raw(string, span) => (string, span), e => unreachable!("unable to get raw of {:?}", e), } } pub(crate) const fn from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self { SassError { kind: SassErrorKind::ParseError { message, loc, unicode, }, } } } #[non_exhaustive] #[derive(Debug, Clone)] pub enum PublicSassErrorKind { ParseError { /// The message related to this parse error. /// /// Error messages should only be used to assist in debugging for the /// end user. They may change significantly between bugfix versions and /// should not be relied on to remain stable. /// /// Error messages do not contain the `Error: ` prefix or pretty-printed /// span and context information as is shown in the `Display` implementation. message: String, loc: SpanLoc, /// Whether or not the user allows unicode characters to be emitted in /// error messages. /// /// This is configurable with [`crate::Options::unicode_error_messages`] unicode: bool, }, /// Sass was unable to find the entry-point file. /// /// Files that cannot be found using `@import`, `@use`, and `@forward` will /// emit [`Self::ParseError`]s IoError(Arc), /// The entry-point file or an imported file was not valid UTF-8. FromUtf8Error(String), } #[derive(Debug, Clone)] enum SassErrorKind { /// A raw error with no additional metadata /// It contains only a `String` message and /// a span Raw(String, Span), ParseError { message: String, loc: SpanLoc, unicode: bool, }, // we put `IoError`s in an `Arc` to allow them to be cloneable IoError(Arc), FromUtf8Error(String), } impl Display for SassError { // TODO: trim whitespace from start of line shown in error // TODO: color errors // TODO: integrate with codemap-diagnostics #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (message, loc, unicode) = match &self.kind { SassErrorKind::ParseError { message, loc, unicode, } => (message, loc, *unicode), SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."), SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s), SassErrorKind::Raw(..) => unreachable!(), }; let first_bar = if unicode { '╷' } else { ',' }; let second_bar = if unicode { '│' } else { '|' }; let third_bar = if unicode { '│' } else { '|' }; let fourth_bar = if unicode { '╵' } else { '\'' }; let line = loc.begin.line + 1; let col = loc.begin.column + 1; writeln!(f, "Error: {}", message)?; let padding = vec![' '; format!("{}", line).len() + 1] .iter() .collect::(); writeln!(f, "{}{}", padding, first_bar)?; writeln!( f, "{} {} {}", line, second_bar, loc.file.source_line(loc.begin.line) )?; writeln!( f, "{}{} {}{}", padding, third_bar, vec![' '; loc.begin.column].iter().collect::(), vec!['^'; loc.end.column.max(loc.begin.column) - loc.begin.column.min(loc.end.column)] .iter() .collect::() )?; writeln!(f, "{}{}", padding, fourth_bar)?; if unicode { writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?; } else { writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?; } Ok(()) } } impl From for Box { #[inline] fn from(error: io::Error) -> Box { Box::new(SassError { kind: SassErrorKind::IoError(Arc::new(error)), }) } } impl From for Box { #[inline] fn from(error: FromUtf8Error) -> Box { Box::new(SassError { kind: SassErrorKind::FromUtf8Error(format!( "Invalid UTF-8 character \"\\x{:X?}\"", error.as_bytes()[0] )), }) } } impl From<(&str, Span)> for Box { #[inline] fn from(error: (&str, Span)) -> Box { Box::new(SassError { kind: SassErrorKind::Raw(error.0.to_owned(), error.1), }) } } impl From<(String, Span)> for Box { #[inline] fn from(error: (String, Span)) -> Box { Box::new(SassError { kind: SassErrorKind::Raw(error.0, error.1), }) } } impl Error for SassError { #[inline] fn description(&self) -> &'static str { "Sass parsing error" } } grass-0.13.4/crates/compiler/src/evaluate/000077500000000000000000000000001465374720000204165ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/evaluate/bin_op.rs000066400000000000000000000421041465374720000222330ustar00rootroot00000000000000use std::cmp::Ordering; use codemap::Span; use crate::{ common::{BinaryOp, QuoteKind}, error::SassResult, unit::Unit, value::{SassNumber, Value}, Options, }; pub(crate) fn add(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match left { Value::Calculation(..) => match right { Value::String(s, quotes) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), quotes, ), _ => { return Err(( format!( "Undefined operation \"{} + {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }, Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", left.inspect(span)?), span, ) .into()) } Value::True | Value::False => match right { Value::String(s, QuoteKind::Quoted) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), QuoteKind::Quoted, ), _ => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, Value::Null => match right { Value::Null => Value::Null, _ => Value::String( right.to_css_string(span, options.is_compressed())?, 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 can't add to any other types? Value::String(..) | Value::Null | Value::List(..) => Value::String( format!( "{}{}", c.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())?, ), QuoteKind::None, ), _ => { return Err(( format!( "Undefined operation \"{} + {}\".", c.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }, Value::String(text, quotes) => match right { Value::String(text2, ..) => Value::String(text + &text2, quotes), _ => Value::String( text + &right.to_css_string(span, options.is_compressed())?, quotes, ), }, Value::List(..) | Value::ArgList(..) => match right { Value::String(s, q) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), q, ), _ => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, }) } pub(crate) fn sub(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match left { Value::Calculation(..) => { return Err(( format!( "Undefined operation \"{} - {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } Value::Null => Value::String( format!("-{}", right.to_css_string(span, options.is_compressed())?), QuoteKind::None, ), Value::Dimension(SassNumber { num, unit, as_slash: _, }) => match right { Value::Dimension(SassNumber { num: num2, unit: unit2, as_slash: _, }) => { if !unit.comparable(&unit2) { return Err( (format!("Incompatible units {} and {}.", unit2, unit), span).into(), ); } if unit == unit2 { Value::Dimension(SassNumber { num: num - num2, unit, as_slash: None, }) } else if unit == Unit::None { Value::Dimension(SassNumber { num: num - num2, unit: unit2, as_slash: None, }) } else if unit2 == Unit::None { Value::Dimension(SassNumber { num: num - num2, unit, as_slash: None, }) } else { Value::Dimension(SassNumber { num: num - num2.convert(&unit2, &unit), unit, as_slash: None, }) } } Value::List(..) | Value::String(..) | Value::True | Value::False | Value::ArgList(..) => Value::String( format!( "{}{}-{}", num.to_string(options.is_compressed()), unit, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", right.inspect(span)?), span, ) .into()) } Value::Color(..) | Value::Calculation(..) => { return Err(( format!( "Undefined operation \"{}{} - {}\".", num.inspect(), unit, right.inspect(span)? ), span, ) .into()) } Value::Null => Value::String( format!("{}{}-", num.to_string(options.is_compressed()), unit), QuoteKind::None, ), }, c @ Value::Color(..) => match right { Value::Dimension(SassNumber { .. }) | Value::Color(..) => { return Err(( format!( "Undefined operation \"{} - {}\".", c.inspect(span)?, right.inspect(span)? ), span, ) .into()) } _ => Value::String( format!( "{}-{}", c.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, Value::String(..) => Value::String( format!( "{}-{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), // todo: can be greatly simplified _ => match right { Value::String(s, q) => Value::String( format!( "{}-{}{}{}", left.to_css_string(span, options.is_compressed())?, q, s, q ), QuoteKind::None, ), Value::Null => Value::String( format!("{}-", left.to_css_string(span, options.is_compressed())?), QuoteKind::None, ), _ => Value::String( format!( "{}-{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, }) } pub(crate) fn mul(left: Value, right: Value, _: &Options, span: Span) -> SassResult { Ok(match left { Value::Dimension(SassNumber { num, unit, as_slash: _, }) => match right { Value::Dimension(SassNumber { num: num2, unit: unit2, as_slash: _, }) => { if unit2 == Unit::None { return Ok(Value::Dimension(SassNumber { num: num * num2, unit, as_slash: None, })); } let n = SassNumber { num, unit, as_slash: None, } * SassNumber { num: num2, unit: unit2, as_slash: None, }; Value::Dimension(n) } _ => { return Err(( format!( "Undefined operation \"{}{} * {}\".", num.inspect(), unit, right.inspect(span)? ), span, ) .into()) } }, _ => { return Err(( format!( "Undefined operation \"{} * {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }) } pub(crate) fn cmp( left: &Value, right: &Value, _: &Options, span: Span, op: BinaryOp, ) -> SassResult { let ordering = match left.cmp(right, span, op)? { Some(ord) => ord, None => return Ok(Value::False), }; Ok(match op { BinaryOp::GreaterThan => match ordering { Ordering::Greater => Value::True, Ordering::Less | Ordering::Equal => Value::False, }, BinaryOp::GreaterThanEqual => match ordering { Ordering::Greater | Ordering::Equal => Value::True, Ordering::Less => Value::False, }, BinaryOp::LessThan => match ordering { Ordering::Less => Value::True, Ordering::Greater | Ordering::Equal => Value::False, }, BinaryOp::LessThanEqual => match ordering { Ordering::Less | Ordering::Equal => Value::True, Ordering::Greater => Value::False, }, _ => unreachable!(), }) } pub(crate) fn single_eq( left: &Value, right: &Value, options: &Options, span: Span, ) -> SassResult { Ok(Value::String( format!( "{}={}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, )) } pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match (left, right) { (Value::Dimension(num1), Value::Dimension(num2)) => { if num2.unit == Unit::None { return Ok(Value::Dimension(SassNumber { num: num1.num / num2.num, unit: num1.unit, as_slash: None, })); } let n = SassNumber { num: num1.num, unit: num1.unit, as_slash: None, } / SassNumber { num: num2.num, unit: num2.unit, as_slash: None, }; Value::Dimension(n) } ( left @ Value::Color(..), right @ (Value::Dimension(SassNumber { .. }) | Value::Color(..)), ) => { return Err(( format!( "Undefined operation \"{} / {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } (left, right) => 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, span: Span) -> SassResult { Ok(match (left, right) { (Value::Dimension(num1), Value::Dimension(num2)) => { if !num1.unit.comparable(&num2.unit) { return Err(( format!("Incompatible units {} and {}.", num1.unit, num2.unit), span, ) .into()); } let new_num = num1.num % num2.num.convert(&num2.unit, &num1.unit); let new_unit = if num1.unit == num2.unit { num1.unit } else if num1.unit == Unit::None { num2.unit } else { num1.unit }; Value::Dimension(SassNumber { num: new_num, unit: new_unit, as_slash: None, }) } (left, right) => { return Err(( format!( "Undefined operation \"{} % {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }) } grass-0.13.4/crates/compiler/src/evaluate/css_tree.rs000066400000000000000000000105041465374720000225730ustar00rootroot00000000000000use std::{ cell::{Ref, RefCell, RefMut}, collections::BTreeMap, }; use crate::ast::CssStmt; #[derive(Debug, Clone)] pub(super) struct CssTree { // None is tombstone stmts: Vec>>, pub parent_to_child: BTreeMap>, pub child_to_parent: BTreeMap, } #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] #[repr(transparent)] pub(super) struct CssTreeIdx(usize); impl CssTree { pub const ROOT: CssTreeIdx = CssTreeIdx(0); pub fn new() -> Self { let mut tree = Self { stmts: Vec::new(), parent_to_child: BTreeMap::new(), child_to_parent: BTreeMap::new(), }; tree.stmts.push(RefCell::new(None)); tree } pub fn get(&self, idx: CssTreeIdx) -> Ref> { self.stmts[idx.0].borrow() } pub fn get_mut(&self, idx: CssTreeIdx) -> RefMut> { self.stmts[idx.0].borrow_mut() } pub fn finish(self) -> Vec { let mut idx = 1; while idx < self.stmts.len() - 1 { if self.stmts[idx].borrow().is_none() || !self.has_children(CssTreeIdx(idx)) { idx += 1; continue; } self.apply_children(CssTreeIdx(idx)); idx += 1; } self.stmts .into_iter() .filter_map(RefCell::into_inner) .collect() } fn apply_children(&self, parent: CssTreeIdx) { for &child in &self.parent_to_child[&parent] { if self.has_children(child) { self.apply_children(child); } match self.stmts[child.0].borrow_mut().take() { Some(child) => self.add_child_to_parent(child, parent), None => continue, }; } } fn has_children(&self, parent: CssTreeIdx) -> bool { self.parent_to_child.contains_key(&parent) } fn add_child_to_parent(&self, child: CssStmt, parent_idx: CssTreeIdx) { RefMut::map(self.stmts[parent_idx.0].borrow_mut(), |parent| { match 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); } }; parent }); } 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(); // todo: we shouldn't take into account children that are invisible parent_children.last() != Some(&child) } pub fn add_stmt(&mut self, child: CssStmt, parent: Option) -> CssTreeIdx { match parent { Some(parent) => self.add_child(child, parent), None => self.add_child(child, Self::ROOT), } } fn add_stmt_inner(&mut self, stmt: CssStmt) -> CssTreeIdx { let idx = CssTreeIdx(self.stmts.len()); self.stmts.push(RefCell::new(Some(stmt))); idx } } grass-0.13.4/crates/compiler/src/evaluate/env.rs000066400000000000000000000425401465374720000215610ustar00rootroot00000000000000use codemap::{Span, Spanned}; use crate::{ ast::{AstForwardRule, Configuration, ConfiguredValue, Mixin}, builtin::modules::{ForwardedModule, Module, ModuleScope, Modules, ShadowedModule}, common::Identifier, error::SassResult, selector::ExtensionStore, value::{SassFunction, Value}, }; use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, sync::Arc, }; type Mutable = Arc>; use super::{scope::Scopes, visitor::CallableContentBlock}; #[derive(Debug, Clone)] pub(crate) struct Environment { pub scopes: Scopes, pub modules: Mutable, pub global_modules: Vec>, pub content: Option>, pub forwarded_modules: Mutable>>, pub imported_modules: Mutable>>, #[allow(clippy::type_complexity)] pub nested_forwarded_modules: Option>>>>>, } 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())), imported_modules: Arc::new(RefCell::new(Vec::new())), nested_forwarded_modules: None, } } 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), imported_modules: Arc::clone(&self.imported_modules), nested_forwarded_modules: self.nested_forwarded_modules.as_ref().map(Arc::clone), } } pub fn for_import(&self) -> Self { Self { scopes: self.scopes.new_closure(), modules: Arc::new(RefCell::new(Modules::new())), global_modules: Vec::new(), content: self.content.as_ref().map(Arc::clone), forwarded_modules: Arc::clone(&self.forwarded_modules), imported_modules: Arc::clone(&self.imported_modules), nested_forwarded_modules: self.nested_forwarded_modules.as_ref().map(Arc::clone), } } pub fn to_dummy_module(&self, span: Span) -> Module { Module::Environment { scope: ModuleScope::new(), upstream: Vec::new(), extension_store: ExtensionStore::new(span), env: self.clone(), } } /// Makes the members forwarded by [module] available in the current /// environment. /// /// This is called when [module] is `@import`ed. pub fn import_forwards(&mut self, _env: Module) { if let Module::Environment { env, .. } = _env { let mut forwarded = env.forwarded_modules; if (*forwarded).borrow().is_empty() { return; } // Omit modules from [forwarded] that are already globally available and // forwarded in this module. let forwarded_modules = Arc::clone(&self.forwarded_modules); if !(*forwarded_modules).borrow().is_empty() { // todo: intermediate name let mut x = Vec::new(); for entry in (*forwarded).borrow().iter() { if !forwarded_modules .borrow() .iter() .any(|module| Arc::ptr_eq(module, entry)) || !self .global_modules .iter() .any(|module| Arc::ptr_eq(module, entry)) { x.push(Arc::clone(entry)); } } forwarded = Arc::new(RefCell::new(x)); } let forwarded_var_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().variables.keys()) .collect::>(); let forwarded_fn_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().functions.keys()) .collect::>(); let forwarded_mixin_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().mixins.keys()) .collect::>(); if self.at_root() { let mut to_remove = Vec::new(); // Hide members from modules that have already been imported or // forwarded that would otherwise conflict with the @imported members. for (idx, module) in (*self.imported_modules).borrow().iter().enumerate() { let shadowed = ShadowedModule::if_necessary( Arc::clone(module), Some(&forwarded_var_names), Some(&forwarded_fn_names), Some(&forwarded_mixin_names), ); if shadowed.is_some() { to_remove.push(idx); } } let mut imported_modules = (*self.imported_modules).borrow_mut(); for &idx in to_remove.iter().rev() { imported_modules.remove(idx); } to_remove.clear(); for (idx, module) in (*self.forwarded_modules).borrow().iter().enumerate() { let shadowed = ShadowedModule::if_necessary( Arc::clone(module), Some(&forwarded_var_names), Some(&forwarded_fn_names), Some(&forwarded_mixin_names), ); if shadowed.is_some() { to_remove.push(idx); } } let mut forwarded_modules = (*self.forwarded_modules).borrow_mut(); for &idx in to_remove.iter().rev() { forwarded_modules.remove(idx); } imported_modules.extend(forwarded.borrow().iter().map(Arc::clone)); forwarded_modules.extend(forwarded.borrow().iter().map(Arc::clone)); } else { self.nested_forwarded_modules .get_or_insert_with(|| { Arc::new(RefCell::new( (0..self.scopes.len()) .map(|_| Arc::new(RefCell::new(Vec::new()))) .collect(), )) }) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .extend(forwarded.borrow().iter().map(Arc::clone)); } // Remove existing member definitions that are now shadowed by the // forwarded modules. for variable in forwarded_var_names { (*self.scopes.variables) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&variable); } self.scopes.last_variable_index = None; for func in forwarded_fn_names { (*self.scopes.functions) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&func); } for mixin in forwarded_mixin_names { (*self.scopes.mixins) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&mixin); } } } pub fn to_implicit_configuration(&self) -> Configuration { let mut configuration = BTreeMap::new(); let variables = (*self.scopes.variables).borrow(); for variables in variables.iter() { let entries = (**variables).borrow(); for (key, value) in entries.iter() { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. configuration.insert(*key, ConfiguredValue::implicit(value.clone())); } } Configuration::implicit(configuration) } pub fn forward_module(&mut self, module: Arc>, rule: AstForwardRule) { let view = ForwardedModule::if_necessary(module, rule); (*self.forwarded_modules).borrow_mut().push(view); // todo: assertnoconflicts } pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) { self.scopes.insert_mixin(name, mixin); } pub fn mixin_exists(&self, name: Identifier) -> bool { self.scopes.mixin_exists(name) } pub fn get_mixin( &self, name: Spanned, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return (*module).borrow().get_mixin(name); } match self.scopes.get_mixin(name) { Ok(v) => Ok(v), Err(e) => { if let Some(v) = self.get_mixin_from_global_modules(name.node) { return Ok(v); } Err(e) } } } pub fn insert_fn(&mut self, func: SassFunction) { self.scopes.insert_fn(func); } pub fn fn_exists(&self, name: Identifier) -> bool { self.scopes.fn_exists(name) } pub fn get_fn( &self, name: Identifier, namespace: Option>, ) -> SassResult> { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return Ok((*module).borrow().get_fn(name)); } Ok(self .scopes .get_fn(name) .or_else(|| self.get_function_from_global_modules(name))) } pub fn var_exists( &self, name: Identifier, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return Ok((*module).borrow().var_exists(name)); } Ok(self.scopes.var_exists(name)) } pub fn get_var( &mut self, name: Spanned, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return (*module).borrow().get_var(name); } match self.scopes.get_var(name) { Ok(v) => Ok(v), Err(e) => { if let Some(v) = self.get_variable_from_global_modules(name.node) { Ok(v) } else { Err(e) } } } } pub fn insert_var( &mut self, name: Spanned, namespace: Option>, value: Value, is_global: bool, in_semi_global_scope: bool, ) -> SassResult<()> { if let Some(namespace) = namespace { let mut modules = (*self.modules).borrow_mut(); let module = modules.get_mut(namespace.node, namespace.span)?; (*module).borrow_mut().update_var(name, value)?; return Ok(()); } if is_global || self.at_root() { // If this module doesn't already contain a variable named [name], try // setting it in a global module. if !self.scopes.global_var_exists(name.node) { let module_with_name = self.from_one_module(name.node, "variable", |module| { if module.borrow().var_exists(*name) { Some(Arc::clone(module)) } else { None } }); if let Some(module_with_name) = module_with_name { module_with_name.borrow_mut().update_var(name, value)?; return Ok(()); } } 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.last_variable_index = Some((name.node, index)); self.scopes.insert_var(index, name.node, value); Ok(()) } pub fn at_root(&self) -> bool { self.scopes.len() == 1 } pub fn scopes_mut(&mut self) -> &mut Scopes { &mut self.scopes } pub fn global_vars(&self) -> Arc>> { self.scopes.global_variables() } pub fn global_mixins(&self) -> Arc>> { self.scopes.global_mixins() } pub fn global_functions(&self) -> Arc>> { self.scopes.global_functions() } fn get_variable_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "variable", |module| { (**module).borrow().get_var_no_err(name) }) } fn get_function_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "function", |module| (**module).borrow().get_fn(name)) } fn get_mixin_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "mixin", |module| { (**module).borrow().get_mixin_no_err(name) }) } pub fn add_module( &mut self, namespace: Option, module: Arc>, span: Span, ) -> SassResult<()> { match namespace { Some(namespace) => { (*self.modules) .borrow_mut() .insert(namespace, module, span)?; } None => { for name in (*self.scopes.global_variables()).borrow().keys() { if (*module).borrow().var_exists(*name) { return Err(( format!("This module and the new module both define a variable named \"${name}\".", name = name) , span).into()); } } self.global_modules.push(module); } } Ok(()) } pub fn to_module(self, extension_store: ExtensionStore) -> Arc> { debug_assert!(self.at_root()); Arc::new(RefCell::new(Module::new_env(self, extension_store))) } fn from_one_module( &self, _name: Identifier, _ty: &str, callback: impl Fn(&Arc>) -> Option, ) -> Option { if let Some(nested_forwarded_modules) = &self.nested_forwarded_modules { for modules in nested_forwarded_modules.borrow().iter().rev() { for module in modules.borrow().iter().rev() { if let Some(value) = callback(module) { return Some(value); } } } } for module in self.imported_modules.borrow().iter() { if let Some(value) = callback(module) { return Some(value); } } let mut value: Option = None; // Object? identity; for module in self.global_modules.iter() { let value_in_module = match callback(module) { Some(v) => v, None => continue, }; value = Some(value_in_module); // Object? identityFromModule = valueInModule is AsyncCallable // ? valueInModule // : module.variableIdentity(name); // if (identityFromModule == identity) continue; // if (value != null) { // var spans = _globalModules.entries.map( // (entry) => callback(entry.key).andThen((_) => entry.value.span)); // throw MultiSpanSassScriptException( // 'This $type is available from multiple global modules.', // '$type use', { // for (var span in spans) // if (span != null) span: 'includes $type' // }); // } // value = valueInModule; // identity = identityFromModule; } value } } grass-0.13.4/crates/compiler/src/evaluate/mod.rs000066400000000000000000000002321465374720000215400ustar00rootroot00000000000000pub(crate) use bin_op::{cmp, div}; pub(crate) use env::Environment; pub use visitor::Visitor; mod bin_op; mod css_tree; mod env; mod scope; mod visitor; grass-0.13.4/crates/compiler/src/evaluate/scope.rs000066400000000000000000000174071465374720000221060ustar00rootroot00000000000000use 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 { pub(crate) variables: Arc>>>>>, pub(crate) mixins: Arc>>>>>, pub(crate) functions: Arc>>>>>, len: Arc>, pub last_variable_index: Option<(Identifier, 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)), last_variable_index: None, } } 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())), last_variable_index: self.last_variable_index, } } pub fn global_variables(&self) -> Arc>> { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); Arc::clone(&(*self.variables).borrow()[0]) } pub fn global_functions(&self) -> Arc>> { Arc::clone(&(*self.functions).borrow()[0]) } pub fn global_mixins(&self) -> Arc>> { Arc::clone(&(*self.mixins).borrow()[0]) } pub fn find_var(&mut self, name: Identifier) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); match self.last_variable_index { Some((prev_name, idx)) if prev_name == name => return Some(idx), _ => {} }; for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() { if (**scope).borrow().contains_key(&name) { self.last_variable_index = Some((name, idx)); 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(); self.last_variable_index = None; } } /// Variables impl Scopes { pub fn insert_var(&mut self, idx: usize, name: Identifier, v: Value) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.variables).borrow_mut()[idx]) .borrow_mut() .insert(name, v) } /// Always insert this variable into the innermost scope /// /// Used, for example, for variables from `@each` and `@for` pub fn insert_var_last(&mut self, name: Identifier, v: Value) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); let last_idx = self.len() - 1; self.last_variable_index = Some((name, last_idx)); (*(*self.variables).borrow_mut()[last_idx]) .borrow_mut() .insert(name, v) } pub fn get_var(&mut self, name: Spanned) -> SassResult { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); match self.last_variable_index { Some((prev_name, idx)) if prev_name == name.node => { return Ok((*(*self.variables).borrow()[idx]).borrow()[&name.node].clone()); } _ => {} }; for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() { match (**scope).borrow().get(&name.node) { Some(var) => { self.last_variable_index = Some((name.node, idx)); 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 } pub fn global_var_exists(&self, name: Identifier) -> bool { self.global_variables().borrow().contains_key(&name) } } /// Mixins impl Scopes { pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.mixins).borrow_mut().last_mut().unwrap()) .borrow_mut() .insert(name, mixin); } pub fn get_mixin(&self, name: Spanned) -> SassResult { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.mixins).borrow().iter().rev() { match (**scope).borrow().get(&name.node) { Some(mixin) => return Ok(mixin.clone()), None => continue, } } Err(("Undefined mixin.", name.span).into()) } pub fn mixin_exists(&self, name: Identifier) -> bool { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.mixins).borrow().iter() { if (**scope).borrow().contains_key(&name) { return true; } } false } } /// Functions impl Scopes { pub fn insert_fn(&mut self, func: SassFunction) { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.functions).borrow_mut().last_mut().unwrap()) .borrow_mut() .insert(func.name(), func); } pub fn get_fn(&self, name: Identifier) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.functions).borrow().iter().rev() { let func = (**scope).borrow().get(&name).cloned(); if func.is_some() { return func; } } None } pub fn fn_exists(&self, name: Identifier) -> bool { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.functions).borrow().iter() { if (**scope).borrow().contains_key(&name) { return true; } } GLOBAL_FUNCTIONS.contains_key(name.as_str()) } } grass-0.13.4/crates/compiler/src/evaluate/visitor.rs000066400000000000000000003170441465374720000224740ustar00rootroot00000000000000use std::{ cell::{Cell, RefCell}, collections::{BTreeMap, BTreeSet, HashSet}, ffi::OsStr, fmt, iter::FromIterator, mem, path::{Path, PathBuf}, rc::Rc, sync::Arc, }; use codemap::{CodeMap, Span, Spanned}; use indexmap::IndexSet; use crate::{ ast::*, builtin::{ meta::if_arguments, modules::{ declare_module_color, declare_module_list, declare_module_map, declare_module_math, declare_module_meta, declare_module_selector, declare_module_string, Module, }, GLOBAL_FUNCTIONS, }, common::{unvendor, BinaryOp, Identifier, ListSeparator, QuoteKind, UnaryOp}, error::{SassError, SassResult}, interner::InternedString, lexer::Lexer, parse::{ AtRootQueryParser, CssParser, KeyframesSelectorParser, SassParser, ScssParser, StylesheetParser, }, selector::{ ComplexSelectorComponent, ExtendRule, ExtendedSelector, ExtensionStore, SelectorList, SelectorParser, }, utils::{to_sentence, trim_ascii}, value::{ ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction, SassMap, SassNumber, UserDefinedFunction, Value, }, ContextFlags, InputSyntax, Options, }; use super::{ bin_op::{add, cmp, div, mul, rem, single_eq, sub}, css_tree::{CssTree, CssTreeIdx}, env::Environment, }; trait UserDefinedCallable { fn name(&self) -> Identifier; fn arguments(&self) -> &ArgumentDeclaration; } impl UserDefinedCallable for AstFunctionDecl { fn name(&self) -> Identifier { self.name.node } fn arguments(&self) -> &ArgumentDeclaration { &self.arguments } } impl UserDefinedCallable for Arc { fn name(&self) -> Identifier { self.name.node } fn arguments(&self) -> &ArgumentDeclaration { &self.arguments } } impl UserDefinedCallable for AstMixin { fn name(&self) -> Identifier { self.name } fn arguments(&self) -> &ArgumentDeclaration { &self.args } } impl UserDefinedCallable for Arc { fn name(&self) -> Identifier { Identifier::from("@content") } fn arguments(&self) -> &ArgumentDeclaration { &self.content.args } } #[derive(Debug, Clone)] pub(crate) struct CallableContentBlock { content: AstContentBlock, env: Environment, } /// Evaluation context of the current execution #[derive(Debug)] pub struct Visitor<'a> { pub(crate) declaration_name: Option, pub(crate) flags: ContextFlags, pub(crate) env: Environment, pub(crate) style_rule_ignoring_at_root: Option, // avoid emitting duplicate warnings for the same span pub(crate) warnings_emitted: HashSet, pub(crate) media_queries: Option>, pub(crate) media_query_sources: Option>, pub(crate) extender: ExtensionStore, /// The complete file path of the current file being visited. Imports are /// resolved relative to this path pub current_import_path: PathBuf, pub(crate) is_plain_css: bool, pub(crate) modules: BTreeMap>>, pub(crate) active_modules: BTreeSet, css_tree: CssTree, parent: Option, configuration: Rc>, import_nodes: Vec, pub options: &'a Options<'a>, pub(crate) map: &'a mut CodeMap, // todo: remove empty_span: Span, import_cache: BTreeMap, /// As a simple heuristic, we don't cache the results of an import unless it /// has been seen in the past. In the majority of cases, files are imported /// at most once. files_seen: BTreeSet, } impl<'a> Visitor<'a> { pub fn new( path: &Path, options: &'a Options<'a>, map: &'a mut CodeMap, empty_span: Span, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, true); let extender = ExtensionStore::new(empty_span); let current_import_path = path.to_path_buf(); Self { declaration_name: None, style_rule_ignoring_at_root: None, flags, warnings_emitted: HashSet::new(), media_queries: None, media_query_sources: None, env: Environment::new(), extender, css_tree: CssTree::new(), parent: None, current_import_path, configuration: Rc::new(RefCell::new(Configuration::empty())), is_plain_css: false, import_nodes: Vec::new(), modules: BTreeMap::new(), active_modules: BTreeSet::new(), options, empty_span, map, import_cache: BTreeMap::new(), files_seen: BTreeSet::new(), } } pub(crate) fn visit_stylesheet(&mut self, mut style_sheet: StyleSheet) -> SassResult<()> { self.active_modules.insert(style_sheet.url.clone()); let was_in_plain_css = self.is_plain_css; self.is_plain_css = style_sheet.is_plain_css; mem::swap(&mut self.current_import_path, &mut style_sheet.url); for stmt in style_sheet.body { let result = self.visit_stmt(stmt)?; debug_assert!(result.is_none()); } mem::swap(&mut self.current_import_path, &mut style_sheet.url); self.is_plain_css = was_in_plain_css; self.active_modules.remove(&style_sheet.url); Ok(()) } pub(crate) fn finish(mut self) -> Vec { let mut finished_tree = self.css_tree.finish(); if self.import_nodes.is_empty() { finished_tree } else { self.import_nodes.append(&mut finished_tree); self.import_nodes } } fn visit_return_rule(&mut self, ret: AstReturn) -> SassResult> { let val = self.visit_expr(ret.val)?; Ok(Some(self.without_slash(val))) } // todo: we really don't have to return Option from all of these children pub(crate) fn visit_stmt(&mut self, stmt: AstStmt) -> SassResult> { match stmt { AstStmt::RuleSet(ruleset) => self.visit_ruleset(ruleset), AstStmt::Style(style) => self.visit_style(style), AstStmt::SilentComment(..) => Ok(None), AstStmt::If(if_stmt) => self.visit_if_stmt(if_stmt), AstStmt::For(for_stmt) => self.visit_for_stmt(for_stmt), AstStmt::Return(ret) => self.visit_return_rule(ret), AstStmt::Each(each_stmt) => self.visit_each_stmt(each_stmt), AstStmt::Media(media_rule) => self.visit_media_rule(media_rule), AstStmt::Include(include_stmt) => self.visit_include_stmt(include_stmt), AstStmt::While(while_stmt) => self.visit_while_stmt(&while_stmt), AstStmt::VariableDecl(decl) => self.visit_variable_decl(decl), AstStmt::LoudComment(comment) => self.visit_loud_comment(comment), AstStmt::ImportRule(import_rule) => self.visit_import_rule(import_rule), AstStmt::FunctionDecl(func) => { self.visit_function_decl(func); Ok(None) } AstStmt::Mixin(mixin) => { self.visit_mixin_decl(mixin); Ok(None) } AstStmt::ContentRule(content_rule) => self.visit_content_rule(content_rule), AstStmt::Warn(warn_rule) => { self.visit_warn_rule(warn_rule)?; Ok(None) } AstStmt::UnknownAtRule(unknown_at_rule) => self.visit_unknown_at_rule(unknown_at_rule), AstStmt::ErrorRule(error_rule) => Err(self.visit_error_rule(error_rule)?), AstStmt::Extend(extend_rule) => self.visit_extend_rule(extend_rule), AstStmt::AtRootRule(at_root_rule) => self.visit_at_root_rule(at_root_rule), AstStmt::Debug(debug_rule) => self.visit_debug_rule(debug_rule), AstStmt::Use(use_rule) => { self.visit_use_rule(use_rule)?; Ok(None) } AstStmt::Forward(forward_rule) => { self.visit_forward_rule(forward_rule)?; Ok(None) } AstStmt::Supports(supports_rule) => { self.visit_supports_rule(supports_rule)?; Ok(None) } } } fn visit_forward_rule(&mut self, forward_rule: AstForwardRule) -> SassResult<()> { let old_config = Rc::clone(&self.configuration); let adjusted_config = Configuration::through_forward(Rc::clone(&old_config), &forward_rule); if !forward_rule.configuration.is_empty() { let new_configuration = self.add_forward_configuration(Rc::clone(&adjusted_config), &forward_rule)?; self.load_module( forward_rule.url.as_path(), Some(Rc::clone(&new_configuration)), false, forward_rule.span, |visitor, module, _| { visitor.env.forward_module(module, forward_rule.clone()); Ok(()) }, )?; Self::remove_used_configuration( &adjusted_config, &new_configuration, &forward_rule .configuration .iter() .filter(|var| !var.is_guarded) .map(|var| var.name.node) .collect(), ); // Remove all the variables that weren't configured by this particular // `@forward` before checking that the configuration is empty. Errors for // outer `with` clauses will be thrown once those clauses finish // executing. let configured_variables: HashSet = forward_rule .configuration .iter() .map(|var| var.name.node) .collect(); let mut to_remove = Vec::new(); for name in (*new_configuration).borrow().values.keys() { if !configured_variables.contains(&name) { to_remove.push(name); } } for name in to_remove { (*new_configuration).borrow_mut().remove(name); } Self::assert_configuration_is_empty(&new_configuration, false)?; } else { self.configuration = adjusted_config; let url = forward_rule.url.clone(); self.load_module( url.as_path(), None, false, forward_rule.span, move |visitor, module, _| { visitor.env.forward_module(module, forward_rule.clone()); Ok(()) }, )?; self.configuration = old_config; } Ok(()) } #[allow(clippy::unnecessary_unwrap)] fn add_forward_configuration( &mut self, config: Rc>, forward_rule: &AstForwardRule, ) -> SassResult>> { let mut new_values = BTreeMap::from_iter((*config).borrow().values.iter()); for variable in &forward_rule.configuration { if variable.is_guarded { let old_value = (*config).borrow_mut().remove(variable.name.node); if old_value.is_some() && !matches!( old_value, Some(ConfiguredValue { value: Value::Null, .. }) ) { new_values.insert(variable.name.node, old_value.unwrap()); continue; } } // todo: superfluous clone? let value = self.visit_expr(variable.expr.node.clone())?; let value = self.without_slash(value); new_values.insert( variable.name.node, ConfiguredValue::explicit(value, variable.expr.span), ); } Ok(Rc::new(RefCell::new( if !(*config).borrow().is_implicit() || (*config).borrow().is_empty() { Configuration::explicit(new_values, forward_rule.span) } else { Configuration::implicit(new_values) }, ))) } /// Remove configured values from [upstream] that have been removed from /// [downstream], unless they match a name in [except]. fn remove_used_configuration( upstream: &Rc>, downstream: &Rc>, except: &HashSet, ) { let mut names_to_remove = Vec::new(); let downstream_keys = (*downstream).borrow().values.keys(); for name in (*upstream).borrow().values.keys() { if except.contains(&name) { continue; } if !downstream_keys.contains(&name) { names_to_remove.push(name); } } for name in names_to_remove { (*upstream).borrow_mut().remove(name); } } fn parenthesize_supports_condition( &mut self, condition: AstSupportsCondition, operator: Option<&str>, ) -> SassResult { match &condition { AstSupportsCondition::Negation(..) => { Ok(format!("({})", self.visit_supports_condition(condition)?)) } AstSupportsCondition::Operation { operator: operator2, .. } if operator2.is_none() || operator2.as_deref() != operator => { Ok(format!("({})", self.visit_supports_condition(condition)?)) } _ => self.visit_supports_condition(condition), } } fn visit_supports_condition(&mut self, condition: AstSupportsCondition) -> SassResult { match condition { AstSupportsCondition::Operation { left, operator, right, } => Ok(format!( "{} {} {}", self.parenthesize_supports_condition(*left, operator.as_deref())?, operator.as_ref().unwrap(), self.parenthesize_supports_condition(*right, operator.as_deref())? )), AstSupportsCondition::Negation(condition) => Ok(format!( "not {}", self.parenthesize_supports_condition(*condition, None)? )), AstSupportsCondition::Interpolation(expr) => { self.evaluate_to_css(expr, QuoteKind::None, self.empty_span) } AstSupportsCondition::Declaration { name, value } => { let old_in_supports_decl = self.flags.in_supports_declaration(); self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, true); let is_custom_property = match &name { AstExpr::String(StringExpr(text, QuoteKind::None), ..) => { text.initial_plain().starts_with("--") } _ => false, }; let result = format!( "({}:{}{})", self.evaluate_to_css(name, QuoteKind::Quoted, self.empty_span)?, if is_custom_property { "" } else { " " }, self.evaluate_to_css(value, QuoteKind::Quoted, self.empty_span)?, ); self.flags .set(ContextFlags::IN_SUPPORTS_DECLARATION, old_in_supports_decl); Ok(result) } AstSupportsCondition::Function { name, args } => Ok(format!( "{}({})", self.perform_interpolation(name, false)?, self.perform_interpolation(args, false)? )), AstSupportsCondition::Anything { contents } => Ok(format!( "({})", self.perform_interpolation(contents, false)?, )), } } fn visit_supports_rule(&mut self, supports_rule: AstSupportsRule) -> SassResult<()> { if self.declaration_name.is_some() { return Err(( "Supports rules may not be used within nested declarations.", supports_rule.span, ) .into()); } let condition = self.visit_supports_condition(supports_rule.condition)?; let css_supports_rule = CssStmt::Supports( SupportsRule { params: condition, body: Vec::new(), }, false, ); let children = supports_rule.body; self.with_parent( css_supports_rule, true, |visitor| { if !visitor.style_rule_exists() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let ruleset = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( ruleset, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, CssStmt::is_style_rule, )?; Ok(()) } fn execute( &mut self, stylesheet: StyleSheet, configuration: Option>>, // todo: different errors based on this _names_in_errors: bool, ) -> SassResult>> { let url = stylesheet.url.clone(); // todo: use canonical url for modules if let Some(already_loaded) = self.modules.get(&stylesheet.url) { let current_configuration = configuration.unwrap_or_else(|| Rc::clone(&self.configuration)); if !current_configuration.borrow().is_implicit() { // if (!_moduleConfigurations[url]!.sameOriginal(currentConfiguration) && // currentConfiguration is ExplicitConfiguration) { // var message = namesInErrors // ? "${p.prettyUri(url)} was already loaded, so it can't be " // "configured using \"with\"." // : "This module was already loaded, so it can't be configured using " // "\"with\"."; // var existingSpan = _moduleNodes[url]?.span; // var configurationSpan = configuration == null // ? currentConfiguration.nodeWithSpan.span // : null; // var secondarySpans = { // if (existingSpan != null) existingSpan: "original load", // if (configurationSpan != null) configurationSpan: "configuration" // }; // throw secondarySpans.isEmpty // ? _exception(message) // : _multiSpanException(message, "new load", secondarySpans); // } } return Ok(Arc::clone(already_loaded)); } let env = Environment::new(); let mut extension_store = ExtensionStore::new(self.empty_span); self.with_environment::, _>(env.new_closure(), |visitor| { let old_parent = visitor.parent; mem::swap(&mut visitor.extender, &mut extension_store); let old_style_rule = visitor.style_rule_ignoring_at_root.take(); let old_media_queries = visitor.media_queries.take(); let old_declaration_name = visitor.declaration_name.take(); let old_in_unknown_at_rule = visitor.flags.in_unknown_at_rule(); let old_at_root_excluding_style_rule = visitor.flags.at_root_excluding_style_rule(); let old_in_keyframes = visitor.flags.in_keyframes(); let old_configuration = if let Some(new_config) = configuration { Some(mem::replace(&mut visitor.configuration, new_config)) } else { None }; visitor.parent = None; visitor.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, false); visitor .flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); visitor.flags.set(ContextFlags::IN_KEYFRAMES, false); visitor.visit_stylesheet(stylesheet)?; // visitor.importer = old_importer; // visitor.stylesheet = old_stylesheet; // visitor.root = old_root; visitor.parent = old_parent; // visitor.end_of_imports = old_end_of_imports; // visitor.out_of_order_imports = old_out_of_order_imports; mem::swap(&mut visitor.extender, &mut extension_store); visitor.style_rule_ignoring_at_root = old_style_rule; visitor.media_queries = old_media_queries; visitor.declaration_name = old_declaration_name; visitor .flags .set(ContextFlags::IN_UNKNOWN_AT_RULE, old_in_unknown_at_rule); visitor.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); visitor .flags .set(ContextFlags::IN_KEYFRAMES, old_in_keyframes); if let Some(old_config) = old_configuration { visitor.configuration = old_config; } Ok(()) })?; let module = env.to_module(extension_store); self.modules.insert(url, Arc::clone(&module)); Ok(module) } pub(crate) fn load_module( &mut self, url: &Path, configuration: Option>>, names_in_errors: bool, span: Span, callback: impl Fn(&mut Self, Arc>, StyleSheet) -> SassResult<()>, ) -> SassResult<()> { let builtin = match url.to_string_lossy().as_ref() { "sass:color" => Some(declare_module_color()), "sass:list" => Some(declare_module_list()), "sass:map" => Some(declare_module_map()), "sass:math" => Some(declare_module_math()), "sass:meta" => Some(declare_module_meta()), "sass:selector" => Some(declare_module_selector()), "sass:string" => Some(declare_module_string()), _ => None, }; if let Some(builtin) = builtin { // todo: lots of ugly unwraps here if configuration.is_some() && !(**configuration.as_ref().unwrap()).borrow().is_implicit() { let msg = if names_in_errors { format!( "Built-in module {} can't be configured.", url.to_string_lossy() ) } else { "Built-in modules can't be configured.".to_owned() }; return Err(( msg, (**configuration.as_ref().unwrap()).borrow().span.unwrap(), ) .into()); } callback( self, Arc::new(RefCell::new(builtin)), StyleSheet::new(false, url.to_path_buf()), )?; return Ok(()); } // todo: decide on naming convention for style_sheet vs stylesheet let stylesheet = self.load_style_sheet(url.to_string_lossy().as_ref(), false, span)?; let canonical_url = self .options .fs .canonicalize(&stylesheet.url) .unwrap_or_else(|_| stylesheet.url.clone()); if self.active_modules.contains(&canonical_url) { return Err(("Module loop: this module is already being loaded.", span).into()); } self.active_modules.insert(canonical_url.clone()); let module = self.execute(stylesheet.clone(), configuration, names_in_errors)?; self.active_modules.remove(&canonical_url); callback(self, module, stylesheet)?; Ok(()) } fn visit_use_rule(&mut self, use_rule: AstUseRule) -> SassResult<()> { let configuration = if use_rule.configuration.is_empty() { Rc::new(RefCell::new(Configuration::empty())) } else { let mut values = BTreeMap::new(); for var in use_rule.configuration { let value = self.visit_expr(var.expr.node)?; let value = self.without_slash(value); values.insert( var.name.node, ConfiguredValue::explicit(value, var.name.span.merge(var.expr.span)), ); } Rc::new(RefCell::new(Configuration::explicit(values, use_rule.span))) }; let span = use_rule.span; let namespace = use_rule .namespace .as_ref() .map(|s| Identifier::from(s.trim_start_matches("sass:"))); self.load_module( &use_rule.url, Some(Rc::clone(&configuration)), false, span, |visitor, module, _| { visitor.env.add_module(namespace, module, span)?; Ok(()) }, )?; Self::assert_configuration_is_empty(&configuration, false)?; Ok(()) } pub(crate) fn assert_configuration_is_empty( config: &Rc>, name_in_error: bool, ) -> SassResult<()> { let config = (**config).borrow(); // By definition, implicit configurations are allowed to only use a subset // of their values. if config.is_empty() || config.is_implicit() { return Ok(()); } let Spanned { node: name, span } = config.first().unwrap(); let msg = if name_in_error { format!( "${name} was not declared with !default in the @used module.", name = name ) } else { "This variable was not declared with !default in the @used module.".to_owned() }; Err((msg, span).into()) } fn visit_import_rule(&mut self, import_rule: AstImportRule) -> SassResult> { for import in import_rule.imports { match import { AstImport::Sass(dynamic_import) => { self.visit_dynamic_import_rule(&dynamic_import)?; } AstImport::Plain(static_import) => self.visit_static_import_rule(static_import)?, } } Ok(None) } /// Searches the current directory of the file then searches in `load_paths` directories /// if the import has not yet been found. /// /// /// #[allow(clippy::cognitive_complexity, clippy::redundant_clone)] pub fn find_import(&self, path: &Path) -> Option { let path_buf = if path.is_absolute() { path.into() } else { self.current_import_path .parent() .unwrap_or_else(|| Path::new("")) .join(path) }; macro_rules! try_path { ($path:expr) => { let path = $path; let dirname = path.parent().unwrap_or_else(|| Path::new("")); let basename = path.file_name().unwrap_or_else(|| OsStr::new("..")); let partial = dirname.join(format!("_{}", basename.to_str().unwrap())); if self.options.fs.is_file(&path) { return Some(path.to_path_buf()); } if self.options.fs.is_file(&partial) { return Some(partial); } }; } if path_buf.extension() == Some(OsStr::new("scss")) || path_buf.extension() == Some(OsStr::new("sass")) || path_buf.extension() == Some(OsStr::new("css")) { let extension = path_buf.extension().unwrap(); try_path!(path_buf.with_extension(format!(".import{}", extension.to_str().unwrap()))); try_path!(path_buf); // todo: consider load paths return None; } macro_rules! try_path_with_extensions { ($path:expr) => { let path = $path; try_path!(path.with_extension("import.sass")); try_path!(path.with_extension("import.scss")); try_path!(path.with_extension("import.css")); try_path!(path.with_extension("sass")); try_path!(path.with_extension("scss")); try_path!(path.with_extension("css")); }; } try_path_with_extensions!(path_buf.clone()); if self.options.fs.is_dir(&path_buf) { try_path_with_extensions!(path_buf.join("index")); } for load_path in &self.options.load_paths { let path_buf = load_path.join(path); try_path_with_extensions!(&path_buf); if self.options.fs.is_dir(&path_buf) { try_path_with_extensions!(path_buf.join("index")); } } None } fn parse_file( &mut self, lexer: Lexer, path: &Path, empty_span: Span, ) -> SassResult { match InputSyntax::for_path(path) { InputSyntax::Scss => ScssParser::new(lexer, self.options, empty_span, path).__parse(), InputSyntax::Sass => SassParser::new(lexer, self.options, empty_span, path).__parse(), InputSyntax::Css => CssParser::new(lexer, self.options, empty_span, path).__parse(), } } fn import_like_node( &mut self, url: &str, _for_import: bool, span: Span, ) -> SassResult { if let Some(name) = self.find_import(url.as_ref()) { let name = self.options.fs.canonicalize(&name).unwrap_or(name); if let Some(style_sheet) = self.import_cache.get(&name) { return Ok(style_sheet.clone()); } let file = self.map.add_file( name.to_string_lossy().into(), String::from_utf8(self.options.fs.read(&name)?)?, ); let old_is_use_allowed = self.flags.is_use_allowed(); self.flags.set(ContextFlags::IS_USE_ALLOWED, true); let style_sheet = self.parse_file(Lexer::new_from_file(&file), &name, file.span.subspan(0, 0))?; self.flags .set(ContextFlags::IS_USE_ALLOWED, old_is_use_allowed); if self.files_seen.contains(&name) { self.import_cache.insert(name, style_sheet.clone()); } else { self.files_seen.insert(name); } return Ok(style_sheet); } Err(("Can't find stylesheet to import.", span).into()) } pub(crate) fn load_style_sheet( &mut self, url: &str, // default=false for_import: bool, span: Span, ) -> SassResult { // todo: import cache self.import_like_node(url, for_import, span) } fn visit_dynamic_import_rule(&mut self, dynamic_import: &AstSassImport) -> SassResult<()> { let stylesheet = self.load_style_sheet(&dynamic_import.url, true, dynamic_import.span)?; let url = stylesheet.url.clone(); if self.active_modules.contains(&url) { return Err(("This file is already being loaded.", dynamic_import.span).into()); } self.active_modules.insert(url.clone()); // If the imported stylesheet doesn't use any modules, we can inject its // CSS directly into the current stylesheet. If it does use modules, we // need to put its CSS into an intermediate [ModifiableCssStylesheet] so // that we can hermetically resolve `@extend`s before injecting it. if stylesheet.uses.is_empty() && stylesheet.forwards.is_empty() { self.visit_stylesheet(stylesheet)?; return Ok(()); } // todo: let loads_user_defined_modules = true; // this todo should be unreachable, as we currently do not push // to stylesheet.uses or stylesheet.forwards // let mut children = Vec::new(); let env = self.env.for_import(); self.with_environment::, _>(env.clone(), |visitor| { let old_parent = visitor.parent; let old_configuration = Rc::clone(&visitor.configuration); if loads_user_defined_modules { visitor.parent = Some(CssTree::ROOT); } // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. if !stylesheet.forwards.is_empty() { visitor.configuration = Rc::new(RefCell::new(env.to_implicit_configuration())); } visitor.visit_stylesheet(stylesheet)?; if loads_user_defined_modules { visitor.parent = old_parent; } visitor.configuration = old_configuration; Ok(()) })?; // Create a dummy module with empty CSS and no extensions to make forwarded // members available in the current import context and to combine all the // CSS from modules used by [stylesheet]. let module = env.to_dummy_module(self.empty_span); self.env.import_forwards(module); if loads_user_defined_modules { // todo: // if (module.transitivelyContainsCss) { // // If any transitively used module contains extensions, we need to // // clone all modules' CSS. Otherwise, it's possible that they'll be // // used or imported from another location that shouldn't have the same // // extensions applied. // await _combineCss(module, // clone: module.transitivelyContainsExtensions) // .accept(this); // } // var visitor = _ImportedCssVisitor(this); // for (var child in children) { // child.accept(visitor); // } } self.active_modules.remove(&url); Ok(()) } fn visit_static_import_rule(&mut self, static_import: AstPlainCssImport) -> SassResult<()> { let import = self.interpolation_to_value(static_import.url, false, false)?; let modifiers = static_import .modifiers .map(|modifiers| self.interpolation_to_value(modifiers, false, false)) .transpose()?; let node = CssStmt::Import(import, modifiers); if self.parent.is_some() && self.parent != Some(CssTree::ROOT) { self.css_tree.add_stmt(node, self.parent); } else { self.import_nodes.push(node); } Ok(()) } fn visit_debug_rule(&mut self, debug_rule: AstDebugRule) -> SassResult> { if self.options.quiet { return Ok(None); } let message = self.visit_expr(debug_rule.value)?; let message = message.inspect(debug_rule.span)?; let loc = self.map.look_up_span(debug_rule.span); self.options.logger.debug(loc, message.as_str()); Ok(None) } fn visit_content_rule(&mut self, content_rule: AstContentRule) -> SassResult> { let span = content_rule.args.span; if let Some(content) = &self.env.content { #[allow(mutable_borrow_reservation_conflict)] self.run_user_defined_callable( MaybeEvaledArguments::Invocation(content_rule.args), Arc::clone(content), &content.env.clone(), span, |content, visitor| { for stmt in content.content.body.clone() { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, )?; } Ok(None) } fn trim_included(&self, nodes: &[CssTreeIdx]) -> CssTreeIdx { if nodes.is_empty() { return CssTree::ROOT; } let mut parent = self.parent; let mut innermost_contiguous: Option = None; for i in 0..nodes.len() { while parent != nodes.get(i).copied() { innermost_contiguous = None; let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); if grandparent.is_none() { unreachable!( "Expected {:?} to be an ancestor of {:?}.", nodes[i], grandparent ) } parent = grandparent; } innermost_contiguous = innermost_contiguous.or(Some(i)); let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); if grandparent.is_none() { unreachable!( "Expected {:?} to be an ancestor of {:?}.", nodes[i], grandparent ) } parent = grandparent; } if parent != Some(CssTree::ROOT) { return CssTree::ROOT; } nodes[innermost_contiguous.unwrap()] } fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult> { let query = match at_root_rule.query.clone() { Some(query) => { let resolved = self.perform_interpolation(query.node, true)?; let span = query.span; let query_toks = Lexer::new_from_string(&resolved, span); AtRootQueryParser::new(query_toks).parse()? } None => AtRootQuery::default(), }; let mut current_parent_idx = self.parent; let mut included = Vec::new(); while let Some(parent_idx) = current_parent_idx { let parent = self.css_tree.get(parent_idx); let grandparent_idx = match &*parent { Some(parent) => { if !query.excludes(parent) { included.push(parent_idx); } self.css_tree.child_to_parent.get(&parent_idx).copied() } None => break, }; current_parent_idx = grandparent_idx; } let root = self.trim_included(&included); // If we didn't exclude any rules, we don't need to use the copies we might // have created. if Some(root) == self.parent { self.with_scope::, _>(false, true, |visitor| { for stmt in at_root_rule.body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; return Ok(None); } let inner_copy = if !included.is_empty() { let inner_copy = self .css_tree .get(*included.first().unwrap()) .as_ref() .map(CssStmt::copy_without_children); let mut outer_copy = self.css_tree.add_stmt(inner_copy.unwrap(), None); for node in &included[1..] { let copy = self .css_tree .get(*node) .as_ref() .map(CssStmt::copy_without_children) .unwrap(); let copy_idx = self.css_tree.add_stmt(copy, None); self.css_tree.link_child_to_parent(outer_copy, copy_idx); outer_copy = copy_idx; } Some(outer_copy) } else { let inner_copy = self .css_tree .get(root) .as_ref() .map(CssStmt::copy_without_children); inner_copy.map(|p| self.css_tree.add_stmt(p, None)) }; let body = mem::take(&mut at_root_rule.body); self.with_scope_for_at_root::, _>(inner_copy, &query, |visitor| { for stmt in body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; Ok(None) } fn with_scope_for_at_root T>( &mut self, new_parent_idx: Option, query: &AtRootQuery, callback: F, ) -> T { let old_parent = self.parent; self.parent = new_parent_idx; let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); if query.excludes_style_rules() { self.flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, true); } let old_media_query_info = if self.media_queries.is_some() && query.excludes_name("media") { Some((self.media_queries.take(), self.media_query_sources.take())) } else { None }; let was_in_keyframes = if self.flags.in_keyframes() && query.excludes_name("keyframes") { let was = self.flags.in_keyframes(); self.flags.set(ContextFlags::IN_KEYFRAMES, false); was } else { self.flags.in_keyframes() }; // todo: // if self.flags.in_unknown_at_rule() && !included.iter().any(|parent| parent is CssAtRule) let res = self.with_scope(false, true, callback); self.parent = old_parent; self.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); if let Some((old_media_queries, old_media_query_sources)) = old_media_query_info { self.media_queries = old_media_queries; self.media_query_sources = old_media_query_sources; } self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); res } fn visit_function_decl(&mut self, fn_decl: AstFunctionDecl) { let name = fn_decl.name.node; // todo: independency let func = SassFunction::UserDefined(UserDefinedFunction { function: Arc::new(fn_decl), name, env: self.env.new_closure(), }); self.env.insert_fn(func); } pub(crate) fn parse_selector_from_string( &mut self, selector_text: &str, allows_parent: bool, allows_placeholder: bool, span: Span, ) -> SassResult { let sel_toks = Lexer::new_from_string(selector_text, span); SelectorParser::new(sel_toks, allows_parent, allows_placeholder, span).parse() } fn visit_extend_rule(&mut self, extend_rule: AstExtendRule) -> SassResult> { if !self.style_rule_exists() || self.declaration_name.is_some() { return Err(( "@extend may only be used within style rules.", extend_rule.span, ) .into()); } let super_selector = self.style_rule_ignoring_at_root.clone().unwrap(); let target_text = self.interpolation_to_value(extend_rule.value, false, true)?; let list = self.parse_selector_from_string(&target_text, false, true, extend_rule.span)?; for complex in list.components { if complex.components.len() != 1 || !complex.components.first().unwrap().is_compound() { // If the selector was a compound selector but not a simple // selector, emit a more explicit error. return Err(("complex selectors may not be extended.", extend_rule.span).into()); } let compound = match complex.components.first() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!("checked by above condition"), }; if compound.components.len() != 1 { return Err(( format!( "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", compound.components.iter().map(ToString::to_string).collect::>().join(", ") ) , extend_rule.span).into()); } self.extender.add_extension( super_selector.clone().into_selector().0, compound.components.first().unwrap(), &ExtendRule { is_optional: extend_rule.is_optional, }, &self.media_queries, extend_rule.span, ); } Ok(None) } fn visit_error_rule(&mut self, error_rule: AstErrorRule) -> SassResult> { let value = self .visit_expr(error_rule.value)? .inspect(error_rule.span)?; Ok((value, error_rule.span).into()) } fn merge_media_queries( queries1: &[MediaQuery], queries2: &[MediaQuery], ) -> Option> { let mut queries = Vec::new(); for query1 in queries1 { for query2 in queries2 { match query1.merge(query2) { MediaQueryMergeResult::Empty => continue, MediaQueryMergeResult::Unrepresentable => return None, MediaQueryMergeResult::Success(result) => queries.push(result), } } } Some(queries) } fn visit_media_queries( &mut self, queries: Interpolation, span: Span, ) -> SassResult> { let resolved = self.perform_interpolation(queries, true)?; CssMediaQuery::parse_list(&resolved, span) } fn visit_media_rule(&mut self, media_rule: AstMedia) -> SassResult> { if self.declaration_name.is_some() { return Err(( "Media rules may not be used within nested declarations.", media_rule.span, ) .into()); } let queries1 = self.visit_media_queries(media_rule.query, media_rule.query_span)?; // todo: superfluous clone? let queries2 = self.media_queries.clone(); let merged_queries = queries2 .as_ref() .and_then(|queries2| Self::merge_media_queries(queries2, &queries1)); let merged_sources = match &merged_queries { Some(merged_queries) if merged_queries.is_empty() => return Ok(None), Some(..) => { let mut set = IndexSet::new(); set.extend(self.media_query_sources.clone().unwrap()); set.extend(self.media_queries.clone().unwrap()); set.extend(queries1.clone()); set } None => IndexSet::new(), }; let children = media_rule.body; let query = merged_queries.clone().unwrap_or_else(|| queries1.clone()); let media_rule = CssStmt::Media( MediaRule { query, body: Vec::new(), }, false, ); self.with_parent( media_rule, true, |visitor| { visitor.with_media_queries( Some(merged_queries.unwrap_or(queries1)), Some(merged_sources.clone()), |visitor| { if !visitor.style_rule_exists() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let ruleset = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( ruleset, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, ) }, |stmt| match stmt { CssStmt::RuleSet { .. } => true, // todo: node.queries.every(mergedSources.contains)) CssStmt::Media(media_rule, ..) => { !merged_sources.is_empty() && media_rule .query .iter() .all(|query| merged_sources.contains(query)) } _ => false, }, )?; Ok(None) } fn visit_unknown_at_rule( &mut self, unknown_at_rule: AstUnknownAtRule, ) -> SassResult> { if self.declaration_name.is_some() { return Err(( "At-rules may not be used within nested declarations.", unknown_at_rule.span, ) .into()); } let name = self.interpolation_to_value(unknown_at_rule.name, false, false)?; let value = unknown_at_rule .value .map(|v| self.interpolation_to_value(v, true, true)) .transpose()?; if unknown_at_rule.body.is_none() { let stmt = CssStmt::UnknownAtRule( UnknownAtRule { name, params: value.unwrap_or_default(), body: Vec::new(), has_body: false, }, false, ); self.css_tree.add_stmt(stmt, self.parent); return Ok(None); } let was_in_keyframes = self.flags.in_keyframes(); let was_in_unknown_at_rule = self.flags.in_unknown_at_rule(); if unvendor(&name) == "keyframes" { self.flags.set(ContextFlags::IN_KEYFRAMES, true); } else { self.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, true); } let children = unknown_at_rule.body.unwrap(); let stmt = CssStmt::UnknownAtRule( UnknownAtRule { name, params: value.unwrap_or_default(), body: Vec::new(), has_body: true, }, false, ); self.with_parent( stmt, true, |visitor| { if !visitor.style_rule_exists() || visitor.flags.in_keyframes() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the at-rule so that // declarations immediately inside it have somewhere to go. // // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let style_rule = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( style_rule, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, CssStmt::is_style_rule, )?; self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); self.flags .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); Ok(None) } pub(crate) fn emit_warning(&mut self, message: &str, span: Span) { if self.options.quiet { return; } let loc = self.map.look_up_span(span); self.options.logger.warn(loc, message); } fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> { if self.warnings_emitted.insert(warn_rule.span) { let value = self.visit_expr(warn_rule.value)?; let message = value.to_css_string(warn_rule.span, self.options.is_compressed())?; self.emit_warning(&message, warn_rule.span); } Ok(()) } fn with_media_queries( &mut self, queries: Option>, sources: Option>, callback: impl FnOnce(&mut Self) -> T, ) -> T { let old_media_queries = self.media_queries.take(); let old_media_query_sources = self.media_query_sources.take(); self.media_queries = queries; self.media_query_sources = sources; let result = callback(self); self.media_queries = old_media_queries; self.media_query_sources = old_media_query_sources; result } fn with_environment T>( &mut self, env: Environment, callback: F, ) -> T { let mut old_env = env; mem::swap(&mut self.env, &mut old_env); let val = callback(self); mem::swap(&mut self.env, &mut old_env); val } fn add_child bool>( &mut self, node: CssStmt, through: Option, ) -> CssTreeIdx { if self.parent.is_none() || self.parent == Some(CssTree::ROOT) { return self.css_tree.add_stmt(node, self.parent); } let mut parent = self.parent.unwrap(); if let Some(through) = through { while parent != CssTree::ROOT && through(self.css_tree.get(parent).as_ref().unwrap()) { let grandparent = self.css_tree.child_to_parent.get(&parent).copied(); debug_assert!( grandparent.is_some(), "through() must return false for at least one parent of $node." ); parent = grandparent.unwrap(); } // If the parent has a (visible) following sibling, we shouldn't add to // the parent. Instead, we should create a copy and add it after the // interstitial sibling. if self.css_tree.has_following_sibling(parent) { let grandparent = self.css_tree.child_to_parent.get(&parent).copied().unwrap(); let parent_node = self .css_tree .get(parent) .as_ref() .map(CssStmt::copy_without_children) .unwrap(); parent = self.css_tree.add_child(parent_node, grandparent); } } self.css_tree.add_child(node, parent) } fn with_parent SassResult<()>, FT: Fn(&CssStmt) -> bool>( &mut self, parent: CssStmt, // default=true scope_when: bool, callback: F, // todo: optional through: FT, ) -> SassResult<()> { let parent_idx = self.add_child(parent, Some(through)); let old_parent = self.parent; self.parent = Some(parent_idx); let result = self.with_scope(false, scope_when, callback); self.parent = old_parent; result } fn with_scope T>( &mut self, // default=false semi_global: bool, // default=true when: bool, callback: F, ) -> T { let semi_global = semi_global && self.flags.in_semi_global_scope(); let was_in_semi_global_scope = self.flags.in_semi_global_scope(); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, semi_global); if !when { let v = callback(self); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); return v; } self.env.scopes_mut().enter_new_scope(); let v = callback(self); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); self.env.scopes_mut().exit_scope(); v } fn with_content( &mut self, content: Option>, callback: impl FnOnce(&mut Self) -> T, ) -> T { let old_content = self.env.content.take(); self.env.content = content; let v = callback(self); self.env.content = old_content; v } fn visit_include_stmt(&mut self, include_stmt: AstInclude) -> SassResult> { let mixin = self .env .get_mixin(include_stmt.name, include_stmt.namespace)?; match mixin { Mixin::Builtin(mixin) => { if include_stmt.content.is_some() { return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); } let args = self.eval_args(include_stmt.args, include_stmt.name.span)?; mixin(args, self)?; Ok(None) } Mixin::UserDefined(mixin, env) => { if include_stmt.content.is_some() && !mixin.has_content { return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); } let AstInclude { args, content, .. } = include_stmt; let old_in_mixin = self.flags.in_mixin(); self.flags.set(ContextFlags::IN_MIXIN, true); let callable_content = content.map(|c| { Arc::new(CallableContentBlock { content: c, env: self.env.new_closure(), }) }); self.run_user_defined_callable::<_, (), _>( MaybeEvaledArguments::Invocation(args), mixin, &env, include_stmt.name.span, |mixin, visitor| { visitor.with_content(callable_content, |visitor| { for stmt in mixin.body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }) }, )?; self.flags.set(ContextFlags::IN_MIXIN, old_in_mixin); Ok(None) } } } fn visit_mixin_decl(&mut self, mixin: AstMixin) { self.env.insert_mixin( mixin.name, Mixin::UserDefined(mixin, self.env.new_closure()), ); } fn visit_each_stmt(&mut self, each_stmt: AstEach) -> SassResult> { let list = self.visit_expr(each_stmt.list)?.as_list(); // todo: not setting semi_global: true maybe means we can't assign to global scope when declared as global self.env.scopes_mut().enter_new_scope(); let mut result = None; 'outer: for val in list { if each_stmt.variables.len() == 1 { let val = self.without_slash(val); self.env .scopes_mut() .insert_var_last(each_stmt.variables[0], val); } else { for (&var, val) in each_stmt.variables.iter().zip( val.as_list() .into_iter() .chain(std::iter::once(Value::Null).cycle()), ) { let val = self.without_slash(val); self.env.scopes_mut().insert_var_last(var, val); } } for stmt in each_stmt.body.clone() { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_for_stmt(&mut self, for_stmt: AstFor) -> SassResult> { let from_span = for_stmt.from.span; let to_span = for_stmt.to.span; let from_number = self .visit_expr(for_stmt.from.node)? .assert_number(from_span)?; let to_number = self.visit_expr(for_stmt.to.node)?.assert_number(to_span)?; if !to_number.unit().comparable(from_number.unit()) { // todo: better error message here return Err(( "to and from values have incompatible units", from_span.merge(to_span), ) .into()); } let from = from_number.num.assert_int(from_span)?; let mut to = to_number .num .convert(to_number.unit(), from_number.unit()) .assert_int(to_span)?; let direction = if from > to { -1 } else { 1 }; if to == i64::MAX || to == i64::MIN { return Err(( "@for loop upper bound exceeds valid integer representation (i64::MAX)", to_span, ) .into()); } if !for_stmt.is_exclusive { to += direction; } if from == to { return Ok(None); } // todo: self.with_scopes self.env.scopes_mut().enter_new_scope(); let mut result = None; let mut i = from; 'outer: while i != to { self.env.scopes_mut().insert_var_last( for_stmt.variable.node, Value::Dimension(SassNumber { num: Number::from(i), unit: from_number.unit().clone(), as_slash: None, }), ); for stmt in for_stmt.body.clone() { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } i += direction; } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_while_stmt(&mut self, while_stmt: &AstWhile) -> SassResult> { self.with_scope(true, true, |visitor| { let mut result = None; 'outer: while visitor .visit_expr(while_stmt.condition.clone())? .is_truthy() { for stmt in while_stmt.body.clone() { let val = visitor.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } } Ok(result) }) } fn visit_if_stmt(&mut self, if_stmt: AstIf) -> SassResult> { let mut clause: Option> = if_stmt.else_clause; for clause_to_check in if_stmt.if_clauses { if self.visit_expr(clause_to_check.condition)?.is_truthy() { clause = Some(clause_to_check.body); break; } } // todo: self.with_scope self.env.scopes_mut().enter_new_scope(); let mut result = None; if let Some(stmts) = clause { for stmt in stmts { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break; } } } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_loud_comment(&mut self, comment: AstLoudComment) -> SassResult> { if self.flags.in_function() { return Ok(None); } // todo: Comments are allowed to appear between CSS imports // if (_parent == _root && _endOfImports == _root.children.length) { // _endOfImports++; // } let comment = CssStmt::Comment( self.perform_interpolation(comment.text, false)?, comment.span, ); self.css_tree.add_stmt(comment, self.parent); Ok(None) } fn visit_variable_decl(&mut self, decl: AstVariableDecl) -> SassResult> { let name = Spanned { node: decl.name, span: decl.span, }; if decl.is_guarded { if decl.namespace.is_none() && self.env.at_root() { let var_override = (*self.configuration).borrow_mut().remove(decl.name); if !matches!( var_override, Some(ConfiguredValue { value: Value::Null, .. }) | None ) { self.env.insert_var( name, None, var_override.unwrap().value, true, self.flags.in_semi_global_scope(), )?; return Ok(None); } } if self.env.var_exists(decl.name, decl.namespace)? { let value = self.env.get_var(name, decl.namespace).unwrap(); if value != Value::Null { return Ok(None); } } } let value = self.visit_expr(decl.value)?; let value = self.without_slash(value); self.env.insert_var( name, decl.namespace, value, decl.is_global, self.flags.in_semi_global_scope(), )?; Ok(None) } fn interpolation_to_value( &mut self, interpolation: Interpolation, // default=false trim: bool, // default=false warn_for_color: bool, ) -> SassResult { let result = self.perform_interpolation(interpolation, warn_for_color)?; Ok(if trim { trim_ascii(&result, true).to_owned() } else { result }) } fn perform_interpolation( &mut self, mut interpolation: Interpolation, // todo check to emit warning if this is true _warn_for_color: bool, ) -> SassResult { let result = match interpolation.contents.len() { 0 => String::new(), 1 => match interpolation.contents.pop() { Some(InterpolationPart::String(s)) => s, Some(InterpolationPart::Expr(e)) => { let span = e.span; let result = self.visit_expr(e.node)?; // todo: span for specific expr self.serialize(result, QuoteKind::None, span)? } None => unreachable!(), }, _ => interpolation .contents .into_iter() .map(|part| match part { InterpolationPart::String(s) => Ok(s), InterpolationPart::Expr(e) => { let span = e.span; let result = self.visit_expr(e.node)?; // todo: span for specific expr self.serialize(result, QuoteKind::None, span) } }) .collect::>()?, }; Ok(result) } fn evaluate_to_css( &mut self, expr: AstExpr, quote: QuoteKind, span: Span, ) -> SassResult { let result = self.visit_expr(expr)?; self.serialize(result, quote, span) } #[allow(clippy::unused_self)] fn without_slash(&mut self, v: Value) -> Value { match v { Value::Dimension(SassNumber { .. }) if v.as_slash().is_some() => { // todo: emit warning. we don't currently because it can be quite loud // self.emit_warning( // Cow::Borrowed("Using / for division is deprecated and will be removed at some point in the future"), // self.empty_span, // ); } _ => {} } v.without_slash() } fn eval_maybe_args( &mut self, args: MaybeEvaledArguments, span: Span, ) -> SassResult { match args { MaybeEvaledArguments::Invocation(args) => self.eval_args(args, span), MaybeEvaledArguments::Evaled(args) => Ok(args), } } fn eval_args( &mut self, arguments: ArgumentInvocation, span: Span, ) -> SassResult { let mut positional = Vec::with_capacity(arguments.positional.len()); for expr in arguments.positional { let val = self.visit_expr(expr)?; positional.push(self.without_slash(val)); } let mut named = BTreeMap::new(); for (key, expr) in arguments.named { let val = self.visit_expr(expr)?; named.insert(key, self.without_slash(val)); } if arguments.rest.is_none() { return Ok(ArgumentResult { positional, named, separator: ListSeparator::Undecided, span, touched: BTreeSet::new(), }); } let rest = self.visit_expr(arguments.rest.unwrap())?; let mut separator = ListSeparator::Undecided; match rest { Value::Map(rest) => self.add_rest_map(&mut named, rest)?, Value::List(elems, list_separator, _) => { let mut list = elems .into_iter() .map(|e| self.without_slash(e)) .collect::>(); positional.append(&mut list); separator = list_separator; } Value::ArgList(arglist) => { // todo: superfluous clone for (&key, value) in arglist.keywords() { named.insert(key, self.without_slash(value.clone())); } let mut list = arglist .elems .into_iter() .map(|e| self.without_slash(e)) .collect::>(); positional.append(&mut list); separator = arglist.separator; } _ => { positional.push(self.without_slash(rest)); } } if arguments.keyword_rest.is_none() { return Ok(ArgumentResult { positional, named, separator: ListSeparator::Undecided, span: arguments.span, touched: BTreeSet::new(), }); } match self.visit_expr(arguments.keyword_rest.unwrap())? { Value::Map(keyword_rest) => { self.add_rest_map(&mut named, keyword_rest)?; Ok(ArgumentResult { positional, named, separator, span: arguments.span, touched: BTreeSet::new(), }) } v => Err(( format!( "Variable keyword arguments must be a map (was {}).", v.inspect(arguments.span)? ), arguments.span, ) .into()), } } fn add_rest_map( &mut self, named: &mut BTreeMap, rest: SassMap, ) -> SassResult<()> { for (key, val) in rest { match key.node { Value::String(text, ..) => { let val = self.without_slash(val); named.insert(Identifier::from(text), val); } _ => { return Err(( // todo: we have to render the map for this error message "Variable keyword argument map must have string keys.", key.span, ) .into()); } } } Ok(()) } fn run_user_defined_callable< F: UserDefinedCallable, V: fmt::Debug, R: FnOnce(F, &mut Self) -> SassResult, >( &mut self, arguments: MaybeEvaledArguments, func: F, env: &Environment, span: Span, run: R, ) -> SassResult { let mut evaluated = self.eval_maybe_args(arguments, span)?; let mut name = func.name().to_string(); if name != "@content" { name.push_str("()"); } self.with_environment(env.new_closure(), |visitor| { visitor.with_scope(false, true, move |visitor| { func.arguments().verify( evaluated.positional.len(), &evaluated.named, evaluated.span, )?; let declared_arguments = &func.arguments().args; let min_len = evaluated.positional.len().min(declared_arguments.len()); let positional_len = evaluated.positional.len(); #[allow(clippy::needless_range_loop)] for i in (0..min_len).rev() { visitor.env.scopes_mut().insert_var_last( declared_arguments[i].name, evaluated.positional.remove(i), ); } // todo: better name for var let additional_declared_args = if declared_arguments.len() > positional_len { &declared_arguments[positional_len..declared_arguments.len()] } else { &[] }; for argument in additional_declared_args { let name = argument.name; let value = evaluated.named.remove(&argument.name).map_or_else( || { // todo: superfluous clone let v = visitor.visit_expr(argument.default.clone().unwrap())?; Ok(visitor.without_slash(v)) }, SassResult::Ok, )?; visitor.env.scopes_mut().insert_var_last(name, value); } let were_keywords_accessed = Rc::new(Cell::new(false)); let num_named_args = evaluated.named.len(); let has_arg_list = if let Some(rest_arg) = func.arguments().rest { let rest = if !evaluated.positional.is_empty() { evaluated.positional } else { Vec::new() }; let arg_list = Value::ArgList(ArgList::new( rest, Rc::clone(&were_keywords_accessed), // todo: superfluous clone evaluated.named.clone(), if evaluated.separator == ListSeparator::Undecided { ListSeparator::Comma } else { ListSeparator::Space }, )); visitor.env.scopes_mut().insert_var_last(rest_arg, arg_list); true } else { false }; let val = run(func, visitor)?; if !has_arg_list || num_named_args == 0 { return Ok(val); } if (*were_keywords_accessed).get() { return Ok(val); } let argument_word = if num_named_args == 1 { "argument" } else { "arguments" }; let argument_names = to_sentence( evaluated .named .keys() .map(|key| format!("${key}", key = key)) .collect(), "or", ); Err(( format!( "No {argument_word} named {argument_names}.", argument_word = argument_word, argument_names = argument_names ), span, ) .into()) }) }) } pub(crate) fn run_function_callable( &mut self, func: SassFunction, arguments: ArgumentInvocation, span: Span, ) -> SassResult { self.run_function_callable_with_maybe_evaled( func, MaybeEvaledArguments::Invocation(arguments), span, ) } pub(crate) fn run_function_callable_with_maybe_evaled( &mut self, func: SassFunction, arguments: MaybeEvaledArguments, span: Span, ) -> SassResult { match func { SassFunction::Builtin(func, _name) => { let evaluated = self.eval_maybe_args(arguments, span)?; let val = func.0(evaluated, self)?; Ok(self.without_slash(val)) } SassFunction::UserDefined(UserDefinedFunction { function, env, .. }) => self .run_user_defined_callable(arguments, function, &env, span, |function, visitor| { for stmt in function.body.clone() { let result = visitor.visit_stmt(stmt)?; if let Some(val) = result { return Ok(val); } } Err(("Function finished without @return.", span).into()) }), SassFunction::Plain { name } => { let has_named; let mut rest = None; // todo: somewhat hacky solution to support plain css fns passed // as strings to `call(..)` let arguments = match arguments { MaybeEvaledArguments::Invocation(args) => { has_named = !args.named.is_empty() || args.keyword_rest.is_some(); rest = args.rest; args.positional .into_iter() .map(|arg| self.evaluate_to_css(arg, QuoteKind::Quoted, span)) .collect::>>()? } MaybeEvaledArguments::Evaled(args) => { has_named = !args.named.is_empty(); args.positional .into_iter() .map(|arg| arg.to_css_string(span, self.options.is_compressed())) .collect::>>()? } }; if has_named { return Err( ("Plain CSS functions don't support keyword arguments.", span).into(), ); } let mut buffer = format!("{}(", name.as_str()); let mut first = true; for argument in arguments { if first { first = false; } else { buffer.push_str(", "); } buffer.push_str(&argument); } if let Some(rest_arg) = rest { let rest = self.visit_expr(rest_arg)?; if !first { buffer.push_str(", "); } buffer.push_str(&self.serialize(rest, QuoteKind::Quoted, span)?); } buffer.push(')'); Ok(Value::String(buffer, QuoteKind::None)) } } } fn visit_list_expr(&mut self, list: ListExpr) -> SassResult { let elems = list .elems .into_iter() .map(|e| { let value = self.visit_expr(e.node)?; Ok(value) }) .collect::>>()?; Ok(Value::List(elems, list.separator, list.brackets)) } fn visit_function_call_expr(&mut self, func_call: FunctionCallExpr) -> SassResult { let name = func_call.name; let func = match self.env.get_fn(name, func_call.namespace)? { Some(func) => func, None => { if let Some(f) = self.options.custom_fns.get(name.as_str()) { SassFunction::Builtin(f.clone(), name) } else if let Some(f) = GLOBAL_FUNCTIONS.get(name.as_str()) { SassFunction::Builtin(f.clone(), name) } else { if func_call.namespace.is_some() { return Err(("Undefined function.", func_call.span).into()); } SassFunction::Plain { name } } } }; let old_in_function = self.flags.in_function(); self.flags.set(ContextFlags::IN_FUNCTION, true); let value = self.run_function_callable(func, (*func_call.arguments).clone(), func_call.span)?; self.flags.set(ContextFlags::IN_FUNCTION, old_in_function); Ok(value) } fn visit_interpolated_func_expr(&mut self, func: InterpolatedFunction) -> SassResult { let InterpolatedFunction { name, arguments: args, span, } = func; let fn_name = self.perform_interpolation(name, false)?; if !args.named.is_empty() || args.keyword_rest.is_some() { return Err(("Plain CSS functions don't support keyword arguments.", span).into()); } let mut buffer = format!("{}(", fn_name); let mut first = true; for arg in args.positional.clone() { if first { first = false; } else { buffer.push_str(", "); } let evaluated = self.evaluate_to_css(arg, QuoteKind::Quoted, span)?; buffer.push_str(&evaluated); } if let Some(rest_arg) = args.rest { let rest = self.visit_expr(rest_arg)?; if !first { buffer.push_str(", "); } buffer.push_str(&self.serialize(rest, QuoteKind::None, span)?); } buffer.push(')'); Ok(Value::String(buffer, QuoteKind::None)) } fn visit_parent_selector(&self) -> Value { match &self.style_rule_ignoring_at_root { Some(selector) => selector.as_selector_list().clone().to_sass_list(), None => Value::Null, } } fn visit_expr(&mut self, expr: AstExpr) -> SassResult { Ok(match expr { AstExpr::Color(color) => Value::Color(color), AstExpr::Number { n, unit } => Value::Dimension(SassNumber { num: n, unit, as_slash: None, }), AstExpr::List(list) => self.visit_list_expr(list)?, AstExpr::String(StringExpr(text, quote), ..) => self.visit_string(text, quote)?, AstExpr::BinaryOp(binop) => self.visit_bin_op( binop.lhs.clone(), binop.op, binop.rhs.clone(), binop.allows_slash, binop.span, )?, AstExpr::True => Value::True, AstExpr::False => Value::False, AstExpr::Calculation { name, args } => { self.visit_calculation_expr(name, args, self.empty_span)? } AstExpr::FunctionCall(func_call) => self.visit_function_call_expr(func_call)?, AstExpr::If(if_expr) => self.visit_ternary((*if_expr).clone())?, AstExpr::InterpolatedFunction(func) => { self.visit_interpolated_func_expr((*func).clone())? } AstExpr::Map(map) => self.visit_map(map)?, AstExpr::Null => Value::Null, AstExpr::Paren(expr) => self.visit_expr((*expr).clone())?, AstExpr::ParentSelector => self.visit_parent_selector(), AstExpr::UnaryOp(op, expr, span) => self.visit_unary_op(op, (*expr).clone(), span)?, AstExpr::Variable { name, namespace } => self.env.get_var(name, namespace)?, AstExpr::Supports(condition) => Value::String( self.visit_supports_condition((*condition).clone())?, QuoteKind::None, ), }) } fn visit_calculation_value( &mut self, expr: AstExpr, in_min_or_max: bool, span: Span, ) -> SassResult { Ok(match expr { AstExpr::Paren(inner) => match &*inner { AstExpr::FunctionCall(FunctionCallExpr { ref name, .. }) if name.as_str().to_ascii_lowercase() == "var" => { let result = self.visit_calculation_value((*inner).clone(), in_min_or_max, span)?; if let CalculationArg::String(text) = result { CalculationArg::String(format!("({})", text)) } else { result } } _ => self.visit_calculation_value((*inner).clone(), in_min_or_max, span)?, }, AstExpr::String(string_expr, _span) => { debug_assert!(string_expr.1 == QuoteKind::None); CalculationArg::Interpolation(self.perform_interpolation(string_expr.0, false)?) } AstExpr::BinaryOp(binop) => SassCalculation::operate_internal( binop.op, self.visit_calculation_value(binop.lhs.clone(), in_min_or_max, span)?, self.visit_calculation_value(binop.rhs.clone(), in_min_or_max, span)?, in_min_or_max, !self.flags.in_supports_declaration(), self.options, span, )?, AstExpr::Number { .. } | AstExpr::Calculation { .. } | AstExpr::Variable { .. } | AstExpr::FunctionCall { .. } | AstExpr::If(..) => { let result = self.visit_expr(expr)?; match result { Value::Dimension(SassNumber { num, unit, as_slash, }) => CalculationArg::Number(SassNumber { num, unit, as_slash, }), Value::Calculation(calc) => CalculationArg::Calculation(calc), Value::String(s, QuoteKind::None) => CalculationArg::String(s), value => { return Err(( format!( "Value {} can't be used in a calculation.", value.inspect(span)? ), span, ) .into()) } } } v => unreachable!("{:?}", v), }) } fn visit_calculation_expr( &mut self, name: CalculationName, args: Vec, span: Span, ) -> SassResult { let mut args = args .into_iter() .map(|arg| self.visit_calculation_value(arg, name.in_min_or_max(), span)) .collect::>>()?; if self.flags.in_supports_declaration() { return Ok(Value::Calculation(SassCalculation::unsimplified( name, args, ))); } match name { CalculationName::Calc => { debug_assert_eq!(args.len(), 1); Ok(SassCalculation::calc(args.remove(0))) } CalculationName::Min => SassCalculation::min(args, self.options, span), CalculationName::Max => SassCalculation::max(args, self.options, span), CalculationName::Clamp => { let min = args.remove(0); let value = if args.is_empty() { None } else { Some(args.remove(0)) }; let max = if args.is_empty() { None } else { Some(args.remove(0)) }; SassCalculation::clamp(min, value, max, self.options, span) } } } fn visit_unary_op(&mut self, op: UnaryOp, expr: AstExpr, span: Span) -> SassResult { let operand = self.visit_expr(expr)?; match op { UnaryOp::Plus => operand.unary_plus(self, span), UnaryOp::Neg => operand.unary_neg(self, span), UnaryOp::Div => operand.unary_div(self, span), UnaryOp::Not => Ok(operand.unary_not()), } } fn visit_ternary(&mut self, if_expr: Ternary) -> SassResult { if_arguments().verify(if_expr.0.positional.len(), &if_expr.0.named, if_expr.0.span)?; let mut positional = if_expr.0.positional; let mut named = if_expr.0.named; let condition = if positional.is_empty() { named.remove(&Identifier::from("condition")).unwrap() } else { positional.remove(0) }; let if_true = if positional.is_empty() { named.remove(&Identifier::from("if_true")).unwrap() } else { positional.remove(0) }; let if_false = if positional.is_empty() { named.remove(&Identifier::from("if_false")).unwrap() } else { positional.remove(0) }; let value = if self.visit_expr(condition)?.is_truthy() { self.visit_expr(if_true)? } else { self.visit_expr(if_false)? }; Ok(self.without_slash(value)) } fn visit_string(&mut self, mut text: Interpolation, quote: QuoteKind) -> SassResult { // Don't use [performInterpolation] here because we need to get the raw text // from strings, rather than the semantic value. let old_in_supports_declaration = self.flags.in_supports_declaration(); self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, false); let result = match text.contents.len() { 0 => String::new(), 1 => match text.contents.pop() { Some(InterpolationPart::String(s)) => s, Some(InterpolationPart::Expr(Spanned { node, span })) => { match self.visit_expr(node)? { Value::String(s, ..) => s, e => self.serialize(e, QuoteKind::None, span)?, } } None => unreachable!(), }, _ => text .contents .into_iter() .map(|part| match part { InterpolationPart::String(s) => Ok(s), InterpolationPart::Expr(Spanned { node, span }) => { match self.visit_expr(node)? { Value::String(s, ..) => Ok(s), e => self.serialize(e, QuoteKind::None, span), } } }) .collect::>()?, }; self.flags.set( ContextFlags::IN_SUPPORTS_DECLARATION, old_in_supports_declaration, ); Ok(Value::String(result, quote)) } fn visit_map(&mut self, map: AstSassMap) -> SassResult { let mut sass_map = SassMap::new(); for pair in map.0 { let key_span = pair.0.span; let key = self.visit_expr(pair.0.node)?; let value = self.visit_expr(pair.1)?; if sass_map.get_ref(&key).is_some() { return Err(("Duplicate key.", key_span).into()); } sass_map.insert( Spanned { node: key, span: key_span, }, value, ); } Ok(Value::Map(sass_map)) } fn visit_bin_op( &mut self, lhs: AstExpr, op: BinaryOp, rhs: AstExpr, allows_slash: bool, span: Span, ) -> SassResult { let left = self.visit_expr(lhs)?; Ok(match op { BinaryOp::SingleEq => { let right = self.visit_expr(rhs)?; single_eq(&left, &right, self.options, span)? } BinaryOp::Or => { if left.is_truthy() { left } else { self.visit_expr(rhs)? } } BinaryOp::And => { if left.is_truthy() { self.visit_expr(rhs)? } else { left } } BinaryOp::Equal => { let right = self.visit_expr(rhs)?; Value::bool(left == right) } BinaryOp::NotEqual => { let right = self.visit_expr(rhs)?; Value::bool(left != right) } BinaryOp::GreaterThan | BinaryOp::GreaterThanEqual | BinaryOp::LessThan | BinaryOp::LessThanEqual => { let right = self.visit_expr(rhs)?; cmp(&left, &right, self.options, span, op)? } BinaryOp::Plus => { let right = self.visit_expr(rhs)?; add(left, right, self.options, span)? } BinaryOp::Minus => { let right = self.visit_expr(rhs)?; sub(left, right, self.options, span)? } BinaryOp::Mul => { let right = self.visit_expr(rhs)?; mul(left, right, self.options, span)? } BinaryOp::Div => { let right = self.visit_expr(rhs)?; let left_is_number = matches!(left, Value::Dimension { .. }); let right_is_number = matches!(right, Value::Dimension { .. }); if left_is_number && right_is_number && allows_slash { let result = div(left.clone(), right.clone(), self.options, span)?; return result.with_slash( left.assert_number(span)?, right.assert_number(span)?, span, ); } else if left_is_number && right_is_number { // todo: emit warning here. it prints too frequently, so we do not currently // self.emit_warning( // Cow::Borrowed(format!( // "Using / for division outside of calc() is deprecated" // )), // span, // ); } div(left, right, self.options, span)? } BinaryOp::Rem => { let right = self.visit_expr(rhs)?; rem(left, right, self.options, span)? } }) } // todo: superfluous taking `expr` by value fn serialize(&mut self, mut expr: Value, quote: QuoteKind, span: Span) -> SassResult { if quote == QuoteKind::None { expr = expr.unquote(); } expr.to_css_string(span, self.options.is_compressed()) } pub(crate) fn visit_ruleset(&mut self, ruleset: AstRuleSet) -> SassResult> { if self.declaration_name.is_some() { return Err(( "Style rules may not be used within nested declarations.", ruleset.span, ) .into()); } let AstRuleSet { selector: ruleset_selector, body: ruleset_body, .. } = ruleset; let selector_text = self.interpolation_to_value(ruleset_selector, true, true)?; if self.flags.in_keyframes() { let span = ruleset.selector_span; let sel_toks = Lexer::new_from_string(&selector_text, span); let parsed_selector = KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; let keyframes_ruleset = CssStmt::KeyframesRuleSet(KeyframesRuleSet { selector: parsed_selector, body: Vec::new(), }); self.with_parent( keyframes_ruleset, true, |visitor| { for stmt in ruleset_body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, CssStmt::is_style_rule, )?; return Ok(None); } let mut parsed_selector = self.parse_selector_from_string( &selector_text, !self.is_plain_css, !self.is_plain_css, ruleset.selector_span, )?; parsed_selector = parsed_selector.resolve_parent_selectors( self.style_rule_ignoring_at_root .as_ref() // todo: this clone should be superfluous(?) .map(|x| x.as_selector_list().clone()), !self.flags.at_root_excluding_style_rule(), )?; // todo: _mediaQueries let selector = self .extender .add_selector(parsed_selector, &self.media_queries); let rule = CssStmt::RuleSet { selector: selector.clone(), body: Vec::new(), is_group_end: false, }; let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); self.flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); let old_style_rule_ignoring_at_root = self.style_rule_ignoring_at_root.take(); self.style_rule_ignoring_at_root = Some(selector); self.with_parent( rule, true, |visitor| { for stmt in ruleset_body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, CssStmt::is_style_rule, )?; self.style_rule_ignoring_at_root = old_style_rule_ignoring_at_root; self.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); self.set_group_end(); Ok(None) } fn set_group_end(&mut self) -> Option<()> { if !self.style_rule_exists() { let children = self .css_tree .parent_to_child .get(&self.parent.unwrap_or(CssTree::ROOT))?; let child = *children.last()?; self.css_tree .get_mut(child) .as_mut() .map(CssStmt::set_group_end)?; } Some(()) } fn style_rule_exists(&self) -> bool { !self.flags.at_root_excluding_style_rule() && self.style_rule_ignoring_at_root.is_some() } pub(crate) fn visit_style(&mut self, style: AstStyle) -> SassResult> { if !self.style_rule_exists() && !self.flags.in_unknown_at_rule() && !self.flags.in_keyframes() { return Err(( "Declarations may only be used within style rules.", style.span, ) .into()); } let is_custom_property = style.is_custom_property(); let mut name = self.interpolation_to_value(style.name, false, true)?; if let Some(declaration_name) = &self.declaration_name { name = format!("{}-{}", declaration_name, name); } if let Some(value) = style .value .map(|s| { SassResult::Ok(Spanned { node: self.visit_expr(s.node)?, span: s.span, }) }) .transpose()? { // If the value is an empty list, preserve it, because converting it to CSS // will throw an error that we want the user to see. if !value.is_blank() || value.is_empty_list() { // todo: superfluous clones? self.css_tree.add_stmt( CssStmt::Style(Style { property: InternedString::get_or_intern(&name), value: Box::new(value), declared_as_custom_property: is_custom_property, }), self.parent, ); } else if name.starts_with("--") { return Err(("Custom property values may not be empty.", style.span).into()); } } let children = style.body; if !children.is_empty() { let old_declaration_name = self.declaration_name.take(); self.declaration_name = Some(name); self.with_scope::, _>(false, true, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; self.declaration_name = old_declaration_name; } Ok(None) } } grass-0.13.4/crates/compiler/src/fs.rs000066400000000000000000000050721465374720000175720ustar00rootroot00000000000000use std::{ io::{self, Error, ErrorKind}, path::{Path, PathBuf}, }; /// A trait to allow replacing the file system lookup mechanisms. /// /// As it stands, this is imperfect: it’s still using the types and some operations from /// `std::path`, which constrain it to the target platform’s norms. This could be ameliorated by /// the use of associated types for `Path` and `PathBuf`, and putting all remaining methods on this /// trait (`is_absolute`, `parent`, `join`, *&c.*); but that would infect too many other APIs to be /// desirable, so we live with it as it is—which is also acceptable, because the motivating example /// use case is mostly using this as an optimisation over the real platform underneath. pub trait Fs: std::fmt::Debug { /// Returns `true` if the path exists on disk and is pointing at a directory. fn is_dir(&self, path: &Path) -> bool; /// Returns `true` if the path exists on disk and is pointing at a regular file. fn is_file(&self, path: &Path) -> bool; /// Read the entire contents of a file into a bytes vector. fn read(&self, path: &Path) -> io::Result>; /// Canonicalize a file path fn canonicalize(&self, path: &Path) -> io::Result { Ok(path.to_path_buf()) } } /// Use [`std::fs`] to read any files from disk. /// /// This is the default file system implementation. #[derive(Debug)] pub struct StdFs; impl Fs for StdFs { #[inline] fn is_file(&self, path: &Path) -> bool { path.is_file() } #[inline] fn is_dir(&self, path: &Path) -> bool { path.is_dir() } #[inline] fn read(&self, path: &Path) -> io::Result> { std::fs::read(path) } #[inline] fn canonicalize(&self, path: &Path) -> io::Result { std::fs::canonicalize(path) } } /// A file system implementation that acts like it’s completely empty. /// /// This may be useful for security as it denies all access to the file system (so `@import` is /// prevented from leaking anything); you’ll need to use [`from_string`][crate::from_string] for /// this to make any sense (since [`from_path`][crate::from_path] would fail to find a file). #[derive(Debug)] pub struct NullFs; impl Fs for NullFs { #[inline] fn is_file(&self, _path: &Path) -> bool { false } #[inline] fn is_dir(&self, _path: &Path) -> bool { false } #[inline] fn read(&self, _path: &Path) -> io::Result> { Err(Error::new( ErrorKind::NotFound, "NullFs, there is no file system", )) } } grass-0.13.4/crates/compiler/src/interner.rs000066400000000000000000000020451465374720000210050ustar00rootroot00000000000000use lasso::{Rodeo, Spur}; use std::cell::RefCell; use std::fmt::{self, Display}; thread_local!(static STRINGS: RefCell> = RefCell::new(Rodeo::default())); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct InternedString(Spur); impl InternedString { pub fn get_or_intern>(s: T) -> Self { Self(STRINGS.with(|interner| interner.borrow_mut().get_or_intern(s))) } #[allow(dead_code)] pub fn resolve(self) -> String { STRINGS.with(|interner| interner.borrow().resolve(&self.0).to_owned()) } #[allow(dead_code)] pub fn is_empty(self) -> bool { self.resolve_ref() == "" } // todo: no need for unsafe here pub fn resolve_ref<'a>(self) -> &'a str { unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) } } } impl Display for InternedString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { STRINGS.with(|interner| write!(f, "{}", interner.borrow().resolve(&self.0))) } } grass-0.13.4/crates/compiler/src/lexer.rs000066400000000000000000000110561465374720000203000ustar00rootroot00000000000000use std::{iter::Peekable, str::Chars, sync::Arc}; use codemap::{File, Span}; const FORM_FEED: char = '\x0C'; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) struct Token { pub kind: char, pos: u32, } #[derive(Debug, Clone)] pub(crate) struct Lexer { buf: Vec, entire_span: Span, cursor: usize, /// If the input this lexer is spanned over is larger than the original span. /// This is possible due to interpolation. is_expanded: bool, } impl Lexer { pub fn raw_text(&self, start: usize) -> String { self.buf[start..self.cursor] .iter() .map(|t| t.kind) .collect() } pub fn next_char_is(&self, c: char) -> bool { matches!(self.peek(), Some(Token { kind, .. }) if kind == c) } /// Gets the span of the character at the given index. If the index is out of /// bounds, it returns the span of the last character. If the input is empty, /// it returns an empty span fn span_at_index(&self, idx: usize) -> Span { if self.is_expanded { return self.entire_span; } let (start, len) = match self.buf.get(idx) { Some(tok) => (tok.pos, tok.kind.len_utf8()), None => match self.buf.last() { Some(tok) => (tok.pos, tok.kind.len_utf8()), None => (0, 0), }, }; self.entire_span .subspan(start as u64, start as u64 + len as u64) } pub fn span_from(&self, start: usize) -> Span { let start = self.span_at_index(start); let end = self.prev_span(); start.merge(end) } pub fn prev_span(&self) -> Span { self.span_at_index(self.cursor.saturating_sub(1)) } pub fn current_span(&self) -> Span { self.span_at_index(self.cursor) } pub fn peek(&self) -> Option { self.buf.get(self.cursor).copied() } /// Peeks the previous token without modifying the peek cursor pub fn peek_previous(&mut self) -> Option { self.buf.get(self.cursor.checked_sub(1)?).copied() } /// Peeks `n` from current peeked position without modifying cursor pub fn peek_n(&self, n: usize) -> Option { self.buf.get(self.cursor + n).copied() } /// Peeks `n` behind current peeked position without modifying cursor pub fn peek_n_backwards(&self, n: usize) -> Option { self.buf.get(self.cursor.checked_sub(n)?).copied() } /// Set cursor to position and reset peek pub fn set_cursor(&mut self, cursor: usize) { self.cursor = cursor; } pub fn cursor(&self) -> usize { self.cursor } } impl Iterator for Lexer { type Item = Token; fn next(&mut self) -> Option { self.buf.get(self.cursor).copied().map(|tok| { self.cursor += 1; tok }) } fn size_hint(&self) -> (usize, Option) { let remaining = self.buf.len() - self.cursor; (remaining, Some(remaining)) } } /// Lex a string into a series of tokens pub(crate) struct TokenLexer<'a> { buf: Peekable>, cursor: u32, } // todo: maybe char indices? impl<'a> TokenLexer<'a> { pub fn new(buf: Peekable>) -> TokenLexer<'a> { Self { buf, cursor: 0 } } } impl<'a> Iterator for TokenLexer<'a> { type Item = Token; fn next(&mut self) -> Option { let kind = match self.buf.next()? { FORM_FEED => '\n', '\r' => { if self.buf.peek() == Some(&'\n') { self.cursor += 1; self.buf.next(); } '\n' } c => c, }; let len = kind.len_utf8() as u32; let pos = self.cursor; self.cursor += len; Some(Token { pos, kind }) } fn size_hint(&self) -> (usize, Option) { self.buf.size_hint() } } impl Lexer { pub fn new_from_file(file: &Arc) -> Self { let buf = TokenLexer::new(file.source().chars().peekable()).collect(); Self::new(buf, file.span, false) } pub fn new_from_string(s: &str, entire_span: Span) -> Self { let is_expanded = s.len() as u64 > entire_span.len(); let buf = TokenLexer::new(s.chars().peekable()).collect(); Self::new(buf, entire_span, is_expanded) } fn new(buf: Vec, entire_span: Span, is_expanded: bool) -> Self { Lexer { buf, cursor: 0, entire_span, is_expanded, } } } grass-0.13.4/crates/compiler/src/lib.rs000066400000000000000000000170501465374720000177270ustar00rootroot00000000000000/*! This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS. This crate targets compatibility with the reference implementation in Dart. If upgrading from the [now deprecated](https://sass-lang.com/blog/libsass-is-deprecated) `libsass`, one may have to modify their stylesheets. These changes will not differ from those necessary to upgrade to `dart-sass`, and in general such changes should be quite rare. This crate is capable of compiling Bootstrap 4 and 5, bulma and bulma-scss, Bourbon, as well as most other large Sass libraries with complete accuracy. For the vast majority of use cases there should be no perceptible differences from the reference implementation. ## Use as library ``` # use grass_compiler as grass; fn main() -> Result<(), Box> { let css = grass::from_string( "a { b { color: &; } }".to_owned(), &grass::Options::default().style(grass::OutputStyle::Compressed) )?; assert_eq!(css, "a b{color:a b}"); Ok(()) } ``` ## Use as binary ```bash cargo install grass grass input.scss ``` */ #![cfg_attr(doc_cfg, feature(doc_cfg))] #![warn(clippy::all, clippy::cargo, clippy::dbg_macro)] #![deny(missing_debug_implementations)] #![allow( clippy::use_self, // filter isn't fallible clippy::manual_filter_map, renamed_and_removed_lints, clippy::unknown_clippy_lints, clippy::single_match, clippy::new_without_default, clippy::single_match_else, clippy::multiple_crate_versions, clippy::wrong_self_convention, clippy::comparison_chain, clippy::unwrap_or_default, clippy::manual_unwrap_or_default, // todo: these should be enabled clippy::arc_with_non_send_sync, // todo: unignore once we bump MSRV clippy::assigning_clones, unknown_lints, )] use std::path::Path; use parse::{CssParser, SassParser, StylesheetParser}; use sass_ast::StyleSheet; use serializer::Serializer; #[cfg(feature = "wasm-exports")] use wasm_bindgen::prelude::*; use codemap::CodeMap; pub use crate::error::{ PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result, }; pub use crate::fs::{Fs, NullFs, StdFs}; pub use crate::logger::{Logger, NullLogger, StdLogger}; pub use crate::options::{InputSyntax, Options, OutputStyle}; pub use crate::{builtin::Builtin, evaluate::Visitor}; pub(crate) use crate::{context_flags::ContextFlags, lexer::Token}; use crate::{lexer::Lexer, parse::ScssParser}; pub mod sass_value { pub use crate::{ ast::ArgumentResult, color::Color, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, unit::{ComplexUnit, Unit}, value::{ ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction, SassMap, SassNumber, Value, }, }; } pub mod sass_ast { pub use crate::ast::*; } pub use codemap; mod ast; mod builtin; mod color; mod common; mod context_flags; mod error; mod evaluate; mod fs; mod interner; mod lexer; mod logger; mod options; mod parse; mod selector; mod serializer; mod unit; mod utils; mod value; fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box { let (message, span) = err.raw(); Box::new(Error::from_loc(message, map.look_up_span(span), unicode)) } pub fn parse_stylesheet>( input: String, file_name: P, options: &Options, ) -> Result { // todo: much of this logic is duplicated in `from_string_with_file_name` let mut map = CodeMap::new(); let path = file_name.as_ref(); let file = map.add_file(path.to_string_lossy().into_owned(), input); let empty_span = file.span.subspan(0, 0); let lexer = Lexer::new_from_file(&file); let input_syntax = options .input_syntax .unwrap_or_else(|| InputSyntax::for_path(path)); let stylesheet = match input_syntax { InputSyntax::Scss => { ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Sass => { SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Css => { CssParser::new(lexer, 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)), }; Ok(stylesheet) } fn from_string_with_file_name>( input: String, file_name: P, options: &Options, ) -> Result { let mut map = CodeMap::new(); let path = file_name.as_ref(); let file = map.add_file(path.to_string_lossy().into_owned(), input); let empty_span = file.span.subspan(0, 0); let lexer = Lexer::new_from_file(&file); let input_syntax = options .input_syntax .unwrap_or_else(|| InputSyntax::for_path(path)); let stylesheet = match input_syntax { InputSyntax::Scss => { ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Sass => { SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Css => { CssParser::new(lexer, 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))?; prev_was_group_end = is_group_end; prev_requires_semicolon = requires_semicolon; } Ok(serializer.finish(prev_requires_semicolon)) } /// Compile CSS from a path /// /// n.b. `grass` does not currently support files or paths that are not valid UTF-8 /// /// ``` /// # use grass_compiler as grass; /// fn main() -> Result<(), Box> { /// let css = grass::from_path("input.scss", &grass::Options::default())?; /// Ok(()) /// } /// ``` #[inline] pub fn from_path>(p: P, options: &Options) -> Result { from_string_with_file_name(String::from_utf8(options.fs.read(p.as_ref())?)?, p, options) } /// Compile CSS from a string /// /// ``` /// # use grass_compiler as grass; /// fn main() -> Result<(), Box> { /// let css = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?; /// assert_eq!(css, "a b {\n color: a b;\n}\n"); /// Ok(()) /// } /// ``` #[inline] pub fn from_string>(input: S, options: &Options) -> Result { from_string_with_file_name(input.into(), "stdin", options) } #[cfg(feature = "wasm-exports")] #[wasm_bindgen(js_name = from_string)] pub fn from_string_js(input: String) -> std::result::Result { from_string(input, &Options::default()).map_err(|e| e.to_string()) } grass-0.13.4/crates/compiler/src/logger.rs000066400000000000000000000024561465374720000204440ustar00rootroot00000000000000use codemap::SpanLoc; use std::fmt::Debug; /// A trait to allow replacing logging mechanisms pub trait Logger: Debug { /// Logs message from a [`@debug`](https://sass-lang.com/documentation/at-rules/debug/) /// statement fn debug(&self, location: SpanLoc, message: &str); /// Logs message from a [`@warn`](https://sass-lang.com/documentation/at-rules/warn/) /// statement fn warn(&self, location: SpanLoc, message: &str); } /// Logs events to standard error, through [`eprintln!`] #[derive(Debug)] pub struct StdLogger; impl Logger for StdLogger { #[inline] fn debug(&self, location: SpanLoc, message: &str) { eprintln!( "{}:{} DEBUG: {}", location.file.name(), location.begin.line + 1, message ); } #[inline] fn warn(&self, location: SpanLoc, message: &str) { eprintln!( "Warning: {}\n ./{}:{}:{}", message, location.file.name(), location.begin.line + 1, location.begin.column + 1 ); } } /// Discards all logs #[derive(Debug)] pub struct NullLogger; impl Logger for NullLogger { #[inline] fn debug(&self, _location: SpanLoc, _message: &str) {} #[inline] fn warn(&self, _location: SpanLoc, _message: &str) {} } grass-0.13.4/crates/compiler/src/options.rs000066400000000000000000000163001465374720000206510ustar00rootroot00000000000000use std::{ collections::HashMap, path::{Path, PathBuf}, }; use crate::{builtin::Builtin, Fs, Logger, StdFs, StdLogger}; /// 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) logger: &'a dyn Logger, pub(crate) style: OutputStyle, pub(crate) load_paths: Vec, pub(crate) allows_charset: bool, pub(crate) unicode_error_messages: bool, pub(crate) quiet: bool, pub(crate) input_syntax: Option, pub(crate) custom_fns: HashMap, } impl Default for Options<'_> { #[inline] fn default() -> Self { Self { fs: &StdFs, logger: &StdLogger, style: OutputStyle::Expanded, load_paths: Vec::new(), allows_charset: true, unicode_error_messages: true, quiet: false, input_syntax: None, custom_fns: HashMap::new(), } } } 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 } /// This option allows you to define how log events should be handled /// /// Be default, [`StdLogger`] is used, which writes all events to standard output. #[must_use] #[inline] pub fn logger(mut self, logger: &'a dyn Logger) -> Self { self.logger = logger; 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. /// /// Setting this option to `true` will stop all logs from reaching the [`crate::Logger`]. /// /// 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: P) -> Self { self.load_paths.push(path.as_ref().to_owned()); 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: &[P]) -> Self { for path in paths { self.load_paths.push(path.as_ref().to_owned()); } 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 } /// Add a custom function accessible from within Sass /// /// See the [`Builtin`] documentation for additional information #[must_use] #[inline] #[cfg(any(feature = "custom-builtin-fns", doc))] #[cfg_attr(doc_cfg, doc(cfg(feature = "custom-builtin-fns")))] pub fn add_custom_fn>(mut self, name: S, func: Builtin) -> Self { self.custom_fns.insert(name.into(), func); 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, PartialEq, Eq, Hash)] 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()) .map(str::to_ascii_lowercase) .as_deref() { Some("css") => Self::Css, Some("sass") => Self::Sass, _ => Self::Scss, } } } #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 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, } grass-0.13.4/crates/compiler/src/parse/000077500000000000000000000000001465374720000177225ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/parse/at_root_query.rs000066400000000000000000000022701465374720000231650ustar00rootroot00000000000000use std::collections::HashSet; use crate::{ast::AtRootQuery, error::SassResult, lexer::Lexer}; use super::BaseParser; pub(crate) struct AtRootQueryParser { toks: Lexer, } impl BaseParser for AtRootQueryParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl AtRootQueryParser { pub fn new(toks: Lexer) -> AtRootQueryParser { AtRootQueryParser { toks } } pub fn parse(&mut self) -> SassResult { self.expect_char('(')?; self.whitespace()?; let include = self.scan_identifier("with", false)?; if !include { self.expect_identifier("without", false)?; } self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let mut names = HashSet::new(); loop { names.insert(self.parse_identifier(false, false)?.to_ascii_lowercase()); self.whitespace()?; if !self.looking_at_identifier() { break; } } self.expect_char(')')?; self.expect_done()?; Ok(AtRootQuery::new(include, names)) } } grass-0.13.4/crates/compiler/src/parse/base.rs000066400000000000000000000511271465374720000212100ustar00rootroot00000000000000use crate::{ error::SassResult, lexer::Lexer, utils::{as_hex, hex_char_for, is_name, is_name_start, opposite_bracket}, Token, }; pub(crate) trait BaseParser { fn toks(&self) -> &Lexer; fn toks_mut(&mut self) -> &mut Lexer; fn whitespace_without_comments(&mut self) { while matches!( self.toks().peek(), Some(Token { kind: ' ' | '\t' | '\n', .. }) ) { self.toks_mut().next(); } } fn whitespace(&mut self) -> SassResult<()> { loop { self.whitespace_without_comments(); if !self.scan_comment()? { break; } } Ok(()) } fn scan_comment(&mut self) -> SassResult { if !matches!(self.toks().peek(), Some(Token { kind: '/', .. })) { return Ok(false); } Ok(match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { self.skip_silent_comment()?; true } Some(Token { kind: '*', .. }) => { self.skip_loud_comment()?; true } _ => false, }) } fn skip_silent_comment(&mut self) -> SassResult<()> { debug_assert!(self.next_matches("//")); self.toks_mut().next(); self.toks_mut().next(); while self.toks().peek().is_some() && !self.toks().next_char_is('\n') { self.toks_mut().next(); } Ok(()) } fn next_matches(&mut self, s: &str) -> bool { for (idx, c) in s.chars().enumerate() { match self.toks().peek_n(idx) { Some(Token { kind, .. }) if kind == c => {} _ => return false, } } true } fn skip_loud_comment(&mut self) -> SassResult<()> { debug_assert!(self.next_matches("/*")); self.toks_mut().next(); self.toks_mut().next(); while let Some(next) = self.toks_mut().next() { if next.kind != '*' { continue; } while self.scan_char('*') {} if self.scan_char('/') { return Ok(()); } } Err(("expected more input.", self.toks().current_span()).into()) } fn scan_char(&mut self, c: char) -> bool { if let Some(Token { kind, .. }) = self.toks().peek() { if kind == c { self.toks_mut().next(); return true; } } false } fn scan(&mut self, s: &str) -> bool { let start = self.toks().cursor(); for c in s.chars() { if !self.scan_char(c) { self.toks_mut().set_cursor(start); return false; } } true } fn expect_whitespace(&mut self) -> SassResult<()> { if !matches!( self.toks().peek(), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) ) && !self.scan_comment()? { return Err(("Expected whitespace.", self.toks().current_span()).into()); } self.whitespace()?; Ok(()) } fn parse_identifier( &mut self, // default=false normalize: bool, // default=false unit: bool, ) -> SassResult { let mut text = String::new(); if self.scan_char('-') { text.push('-'); if self.scan_char('-') { text.push('-'); self.parse_identifier_body(&mut text, normalize, unit)?; return Ok(text); } } match self.toks().peek() { Some(Token { kind: '_', .. }) if normalize => { self.toks_mut().next(); text.push('-'); } Some(Token { kind, .. }) if is_name_start(kind) => { self.toks_mut().next(); text.push(kind); } Some(Token { kind: '\\', .. }) => { text.push_str(&self.parse_escape(true)?); } Some(..) | None => { return Err(("Expected identifier.", self.toks().current_span()).into()) } } self.parse_identifier_body(&mut text, normalize, unit)?; Ok(text) } fn parse_identifier_body( &mut self, buffer: &mut String, normalize: bool, unit: bool, ) -> SassResult<()> { while let Some(tok) = self.toks().peek() { if unit && tok.kind == '-' { // Disallow `-` followed by a dot or a digit digit in units. let second = match self.toks().peek_n(1) { Some(v) => v, None => break, }; if second.kind == '.' || second.kind.is_ascii_digit() { break; } self.toks_mut().next(); buffer.push('-'); } else if normalize && tok.kind == '_' { buffer.push('-'); self.toks_mut().next(); } else if is_name(tok.kind) { self.toks_mut().next(); buffer.push(tok.kind); } else if tok.kind == '\\' { buffer.push_str(&self.parse_escape(false)?); } else { break; } } Ok(()) } fn parse_escape(&mut self, identifier_start: bool) -> SassResult { let start = self.toks().cursor(); 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()), }; if first.kind == '\n' { return Err(("Expected escape sequence.", self.toks().current_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; 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 { value = first.kind as u32; self.toks_mut().next(); } let c = std::char::from_u32(value) .ok_or_else(|| ("Invalid Unicode code point.", self.toks().span_from(start)))?; 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(..) | 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(..) | None => Err((format!("expected {}.", msg), self.toks().prev_span()).into()), } } fn parse_string(&mut self) -> SassResult { let quote = match self.toks_mut().next() { Some(Token { kind: q @ ('\'' | '"'), .. }) => q, Some(..) | 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}.", quote = quote), self.toks().current_span(), ) .into()); } Ok(buffer) } fn consume_escaped_char(&mut self) -> SassResult { self.expect_char('\\')?; match self.toks().peek() { None => Ok('\u{FFFD}'), Some(Token { kind: '\n' | '\r', .. }) => Err(("Expected escape sequence.", self.toks().current_span()).into()), Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => { let mut value = 0; for _ in 0..6 { let next = match self.toks().peek() { Some(c) => c, None => break, }; if !next.kind.is_ascii_hexdigit() { break; } self.toks_mut().next(); value = (value << 4) + as_hex(next.kind); } if self.toks().peek().is_some() && self.toks().peek().unwrap().kind.is_ascii_whitespace() { self.toks_mut().next(); } if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF { Ok('\u{FFFD}') } else { Ok(char::from_u32(value).unwrap()) } } Some(Token { kind, .. }) => { self.toks_mut().next(); Ok(kind) } } } fn declaration_value(&mut self, allow_empty: bool) -> SassResult { let mut buffer = String::new(); let mut brackets = Vec::new(); let mut wrote_newline = false; while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { buffer.push_str(&self.parse_escape(true)?); wrote_newline = false; } '"' | '\'' => { buffer.push_str(&self.fallible_raw_text(Self::parse_string)?); wrote_newline = false; } '/' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) { buffer.push_str(&self.fallible_raw_text(Self::skip_loud_comment)?); } else { buffer.push('/'); self.toks_mut().next(); } wrote_newline = false; } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { let s = self.parse_identifier(false, false)?; buffer.push_str(&s); } else { buffer.push('#'); self.toks_mut().next(); } wrote_newline = false; } c @ (' ' | '\t') => { if wrote_newline || !self .toks() .peek_n(1) .map_or(false, |tok| tok.kind.is_ascii_whitespace()) { buffer.push(c); } self.toks_mut().next(); } '\n' | '\r' => { if !wrote_newline { buffer.push('\n'); } wrote_newline = true; self.toks_mut().next(); } '[' | '(' | '{' => { buffer.push(tok.kind); self.toks_mut().next(); brackets.push(opposite_bracket(tok.kind)); wrote_newline = false; } ']' | ')' | '}' => { if let Some(end) = brackets.pop() { buffer.push(tok.kind); self.expect_char(end)?; } else { break; } wrote_newline = false; } ';' => { if brackets.is_empty() { break; } self.toks_mut().next(); buffer.push(';'); wrote_newline = false; } 'u' | 'U' => { if let Some(url) = self.try_parse_url()? { buffer.push_str(&url); } else { buffer.push(tok.kind); self.toks_mut().next(); } wrote_newline = false; } c => { if self.looking_at_identifier() { buffer.push_str(&self.parse_identifier(false, false)?); } else { self.toks_mut().next(); buffer.push(c); } wrote_newline = false; } } } if let Some(last) = brackets.pop() { self.expect_char(last)?; } if !allow_empty && buffer.is_empty() { return Err(("Expected token.", self.toks().current_span()).into()); } Ok(buffer) } /// Returns whether the scanner is immediately before a plain CSS identifier. /// /// This is based on [the CSS algorithm][], but it assumes all backslashes /// start escapes. /// /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier fn looking_at_identifier(&self) -> bool { match self.toks().peek() { Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true, Some(Token { kind: '-', .. }) => {} Some(..) | None => return false, } match self.toks().peek_n(1) { Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => true, Some(..) | None => false, } } fn try_parse_url(&mut self) -> SassResult> { let start = self.toks().cursor(); if !self.scan_identifier("url", false)? { return Ok(None); } if !self.scan_char('(') { self.toks_mut().set_cursor(start); return Ok(None); } self.whitespace()?; // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. let mut buffer = "url(".to_owned(); while let Some(next) = self.toks().peek() { match next.kind { '\\' => { buffer.push_str(&self.parse_escape(false)?); } '!' | '#' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { self.toks_mut().next(); buffer.push(next.kind); } ')' => { self.toks_mut().next(); buffer.push(next.kind); return Ok(Some(buffer)); } ' ' | '\t' | '\n' | '\r' => { self.whitespace_without_comments(); if !self.toks().next_char_is(')') { break; } } _ => break, } } self.toks_mut().set_cursor(start); Ok(None) } fn raw_text(&mut self, func: impl Fn(&mut Self) -> T) -> String { let start = self.toks().cursor(); func(self); self.toks().raw_text(start) } fn fallible_raw_text( &mut self, func: impl Fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks().cursor(); func(self)?; Ok(self.toks().raw_text(start)) } /// Peeks to see if the `ident` is at the current position. If it is, /// consume the identifier fn scan_identifier( &mut self, ident: &'static str, // default=false case_sensitive: bool, ) -> SassResult { if !self.looking_at_identifier() { return Ok(false); } let start = self.toks().cursor(); if self.consume_identifier(ident, case_sensitive)? && !self.looking_at_identifier_body() { Ok(true) } else { self.toks_mut().set_cursor(start); Ok(false) } } fn consume_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult { for c in ident.chars() { if !self.scan_ident_char(c, case_sensitive)? { return Ok(false); } } Ok(true) } fn scan_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult { let matches = |actual: char| { if case_sensitive { actual == c } else { actual.to_ascii_lowercase() == c.to_ascii_lowercase() } }; Ok(match self.toks().peek() { Some(Token { kind, .. }) if matches(kind) => { self.toks_mut().next(); true } Some(Token { kind: '\\', .. }) => { let start = self.toks().cursor(); if matches(self.consume_escaped_char()?) { return Ok(true); } self.toks_mut().set_cursor(start); false } Some(..) | None => false, }) } fn expect_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<()> { if self.scan_ident_char(c, case_sensitive)? { return Ok(()); } Err((format!("Expected \"{}\".", c), self.toks().current_span()).into()) } fn looking_at_identifier_body(&mut self) -> bool { matches!(self.toks().peek(), Some(t) if is_name(t.kind) || t.kind == '\\') } fn parse_variable_name(&mut self) -> SassResult { self.expect_char('$')?; self.parse_identifier(true, false) } fn expect_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<()> { let start = self.toks().cursor(); for c in ident.chars() { if !self.scan_ident_char(c, case_sensitive)? { return Err(( format!("Expected \"{}\".", ident), self.toks_mut().span_from(start), ) .into()); } } if !self.looking_at_identifier_body() { return Ok(()); } Err(( format!("Expected \"{}\".", ident), self.toks_mut().span_from(start), ) .into()) } // todo: not real impl fn expect_done(&mut self) -> SassResult<()> { debug_assert!(self.toks().peek().is_none()); Ok(()) } fn spaces(&mut self) { while self.toks().next_char_is(' ') || self.toks().next_char_is('\t') { self.toks_mut().next(); } } } grass-0.13.4/crates/compiler/src/parse/css.rs000066400000000000000000000142021465374720000210570ustar00rootroot00000000000000use std::{collections::BTreeMap, path::Path, sync::Arc}; use 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, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, } impl<'a> BaseParser for CssParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &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(&self) -> bool { true } fn is_indented(&self) -> bool { false } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn current_indentation(&self) -> usize { 0 } fn empty_span(&self) -> Span { self.empty_span } const IDENTIFIER_LIKE: Option SassResult>> = Some(Self::parse_identifier_like); fn parse_at_rule( &mut self, _child: fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks.cursor(); self.expect_char('@')?; let name = self.parse_interpolated_identifier()?; self.whitespace()?; match name.as_plain() { Some("at-root") | Some("content") | Some("debug") | Some("each") | Some("error") | Some("extend") | Some("for") | Some("function") | Some("if") | Some("include") | Some("mixin") | Some("return") | Some("warn") | Some("while") => { self.almost_any_value(false)?; Err(( "This at-rule isn't allowed in plain CSS.", self.toks.span_from(start), ) .into()) } Some("import") => self.parse_css_import_rule(start), Some("media") => self.parse_media_rule(start), Some("-moz-document") => self._parse_moz_document_rule(name), Some("supports") => self.parse_supports_rule(), _ => self.unknown_at_rule(name, start), } } } impl<'a> CssParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { CssParser { toks, path: file_name, empty_span, flags: ContextFlags::empty(), options, } } fn parse_css_import_rule(&mut self, _start: usize) -> SassResult { let url_start = self.toks.cursor(); let url = if self.toks.next_char_is('u') || self.toks.next_char_is('U') { self.parse_dynamic_url()? .span(self.toks.span_from(url_start)) } else { let string = self.parse_interpolated_string()?; AstExpr::String( StringExpr(string.node.as_interpolation(true), QuoteKind::None), string.span, ) .span(string.span) }; self.whitespace()?; let modifiers = self.try_import_modifiers()?; self.expect_statement_separator(Some("@import rule"))?; Ok(AstStmt::ImportRule(AstImportRule { imports: vec![AstImport::Plain(AstPlainCssImport { url: Interpolation::new_with_expr(url), modifiers, span: self.toks.span_from(url_start), })], })) } fn parse_identifier_like(&mut self) -> SassResult> { let start = self.toks.cursor(); let identifier = self.parse_interpolated_identifier()?; let plain = identifier.as_plain().unwrap(); let lower = plain.to_ascii_lowercase(); if let Some(special_fn) = ValueParser::try_parse_special_function(self, &lower, start)? { return Ok(special_fn); } let before_args = self.toks.cursor(); if !self.scan_char('(') { let span = self.toks.span_from(start); return Ok(AstExpr::String(StringExpr(identifier, QuoteKind::None), span).span(span)); } let allow_empty_second_arg = lower == "var"; let mut arguments = Vec::new(); if !self.scan_char(')') { loop { self.whitespace()?; let arg_start = self.toks.cursor(); if allow_empty_second_arg && arguments.len() == 1 && self.toks.next_char_is(')') { arguments.push(AstExpr::String( StringExpr(Interpolation::new_plain(String::new()), QuoteKind::None), self.toks.span_from(arg_start), )); break; } arguments.push(self.parse_expression_until_comma(true)?.node); self.whitespace()?; if !self.scan_char(',') { break; } } self.expect_char(')')?; } let span = self.toks.span_from(start); if DISALLOWED_PLAIN_CSS_FUNCTION_NAMES.contains(plain) { return Err(("This function isn't allowed in plain CSS.", span).into()); } Ok( AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: identifier, arguments: ArgumentInvocation { positional: arguments, named: BTreeMap::new(), rest: None, keyword_rest: None, span: self.toks.span_from(before_args), }, span, })) .span(span), ) } } grass-0.13.4/crates/compiler/src/parse/keyframes.rs000066400000000000000000000071371465374720000222660ustar00rootroot00000000000000use std::fmt; use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, Token}; use super::BaseParser; impl fmt::Display for KeyframesSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { KeyframesSelector::To => f.write_str("to"), KeyframesSelector::From => f.write_str("from"), KeyframesSelector::Percent(p) => write!(f, "{}%", p), } } } pub(crate) struct KeyframesSelectorParser { toks: Lexer, } impl BaseParser for KeyframesSelectorParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl KeyframesSelectorParser { pub fn new(toks: Lexer) -> KeyframesSelectorParser { KeyframesSelectorParser { toks } } pub fn parse_keyframes_selector(&mut self) -> SassResult> { let mut selectors = Vec::new(); loop { self.whitespace()?; if self.looking_at_identifier() { if self.scan_identifier("to", false)? { selectors.push(KeyframesSelector::To); } else if self.scan_identifier("from", false)? { selectors.push(KeyframesSelector::From); } else { return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into()); } } else { selectors.push(self.parse_percentage_selector()?); } self.whitespace()?; if !self.scan_char(',') { break; } } Ok(selectors) } fn parse_percentage_selector(&mut self) -> SassResult { let mut buffer = String::new(); if self.scan_char('+') { buffer.push('+'); } if !matches!( self.toks.peek(), Some(Token { kind: '0'..='9' | '.', .. }) ) { return Err(("Expected number.", self.toks.current_span()).into()); } 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); } } if self.scan_ident_char('e', false)? { buffer.push('e'); if matches!( self.toks.peek(), Some(Token { kind: '+' | '-', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } if !matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { return Err(("Expected digit.", self.toks.current_span()).into()); } while matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } } self.expect_char('%')?; Ok(KeyframesSelector::Percent(buffer.into_boxed_str())) } } grass-0.13.4/crates/compiler/src/parse/media_query.rs000066400000000000000000000102001465374720000225650ustar00rootroot00000000000000use crate::{ast::MediaQuery, error::SassResult, lexer::Lexer}; use super::BaseParser; pub(crate) struct MediaQueryParser { pub toks: Lexer, } impl BaseParser for MediaQueryParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl MediaQueryParser { pub fn new(toks: Lexer) -> MediaQueryParser { MediaQueryParser { toks } } pub fn parse(&mut self) -> SassResult> { let mut queries = Vec::new(); loop { self.whitespace()?; queries.push(self.parse_media_query()?); self.whitespace()?; if !self.scan_char(',') { break; } } if self.toks.next().is_some() { return Err(("expected no more input.", self.toks.current_span()).into()); } Ok(queries) } fn parse_media_query(&mut self) -> SassResult { if self.toks.next_char_is('(') { let mut conditions = vec![self.parse_media_in_parens()?]; self.whitespace()?; let mut conjunction = true; if self.scan_identifier("and", false)? { self.expect_whitespace()?; conditions.append(&mut self.parse_media_logic_sequence("and")?); } else if self.scan_identifier("or", false)? { self.expect_whitespace()?; conjunction = false; conditions.append(&mut self.parse_media_logic_sequence("or")?); } return Ok(MediaQuery::condition(conditions, conjunction)); } let mut modifier: Option = None; let media_type: Option; let identifier1 = self.parse_identifier(false, false)?; if identifier1.to_ascii_lowercase() == "not" { self.expect_whitespace()?; if !self.looking_at_identifier() { return Ok(MediaQuery::condition( vec![format!("(not {})", self.parse_media_in_parens()?)], true, )); } } self.whitespace()?; if !self.looking_at_identifier() { return Ok(MediaQuery::media_type(Some(identifier1), None, None)); } let identifier2 = self.parse_identifier(false, false)?; if identifier2.to_ascii_lowercase() == "and" { self.expect_whitespace()?; media_type = Some(identifier1); } else { self.whitespace()?; modifier = Some(identifier1); media_type = Some(identifier2); if self.scan_identifier("and", false)? { // For example, "@media only screen and ..." self.expect_whitespace()?; } else { // For example, "@media only screen {" return Ok(MediaQuery::media_type(media_type, modifier, None)); } } // We've consumed either `IDENTIFIER "and"` or // `IDENTIFIER IDENTIFIER "and"`. if self.scan_identifier("not", false)? { // For example, "@media screen and not (...) {" self.expect_whitespace()?; return Ok(MediaQuery::media_type( media_type, modifier, Some(vec![format!("(not {})", self.parse_media_in_parens()?)]), )); } Ok(MediaQuery::media_type( media_type, modifier, Some(self.parse_media_logic_sequence("and")?), )) } fn parse_media_in_parens(&mut self) -> SassResult { self.expect_char('(')?; let result = format!("({})", self.declaration_value(false)?); self.expect_char(')')?; Ok(result) } fn parse_media_logic_sequence(&mut self, operator: &'static str) -> SassResult> { let mut result = Vec::new(); loop { result.push(self.parse_media_in_parens()?); self.whitespace()?; if !self.scan_identifier(operator, false)? { return Ok(result); } self.expect_whitespace()?; } } } grass-0.13.4/crates/compiler/src/parse/mod.rs000066400000000000000000000016741465374720000210570ustar00rootroot00000000000000use crate::ast::*; pub(crate) use at_root_query::AtRootQueryParser; pub(crate) use base::BaseParser; pub(crate) use css::CssParser; pub(crate) use keyframes::KeyframesSelectorParser; pub(crate) use media_query::MediaQueryParser; pub(crate) use sass::SassParser; pub(crate) use scss::ScssParser; pub(crate) use stylesheet::StylesheetParser; mod at_root_query; mod base; mod css; mod keyframes; mod media_query; mod sass; mod scss; mod stylesheet; mod value; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum DeclarationOrBuffer { Stmt(AstStmt), Buffer(Interpolation), } /// Names that functions are not allowed to have pub(super) const RESERVED_IDENTIFIERS: [&str; 8] = [ "calc", "element", "expression", "url", "and", "or", "not", "clamp", ]; #[derive(Debug, Clone)] pub(crate) enum VariableDeclOrInterpolation { VariableDecl(AstVariableDecl), Interpolation(Interpolation), } grass-0.13.4/crates/compiler/src/parse/sass.rs000066400000000000000000000403741465374720000212510ustar00rootroot00000000000000use std::path::Path; use codemap::Span; use crate::{ast::*, error::SassResult, lexer::Lexer, ContextFlags, Options, Token}; use super::{BaseParser, StylesheetParser}; pub(crate) struct SassParser<'a> { pub toks: Lexer, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, pub current_indentation: usize, pub next_indentation: Option, pub spaces: Option, pub next_indentation_end: Option, } impl<'a> BaseParser for SassParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &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(&self) -> bool { false } fn is_indented(&self) -> bool { true } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn current_indentation(&self) -> usize { self.current_indentation } fn empty_span(&self) -> Span { self.empty_span } fn parse_style_rule_selector(&mut self) -> SassResult { let mut buffer = Interpolation::new(); loop { buffer.add_interpolation(self.almost_any_value(true)?); buffer.add_char('\n'); if !(buffer.trailing_string().trim_end().ends_with(',') && self.scan_char('\n')) { break; } } Ok(buffer) } fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> { if !self.at_end_of_statement() { self.expect_newline()?; } if self.peek_indentation()? <= self.current_indentation { return Ok(()); } // todo: position: _nextIndentationEnd!.position // todo: error message, "Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}." Err(("Nothing may be indented here", self.toks.current_span()).into()) } fn at_end_of_statement(&self) -> bool { matches!(self.toks.peek(), Some(Token { kind: '\n', .. }) | None) } fn looking_at_children(&mut self) -> SassResult { Ok(self.at_end_of_statement() && self.peek_indentation()? > self.current_indentation) } fn scan_else(&mut self, if_indentation: usize) -> SassResult { if self.peek_indentation()? != if_indentation { return Ok(false); } let start = self.toks.cursor(); let start_indentation = self.current_indentation; let start_next_indentation = self.next_indentation; let start_next_indentation_end = self.next_indentation_end; self.read_indentation()?; if self.scan_char('@') && self.scan_identifier("else", false)? { return Ok(true); } self.toks.set_cursor(start); self.current_indentation = start_indentation; self.next_indentation = start_next_indentation; self.next_indentation_end = start_next_indentation_end; Ok(false) } fn parse_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult> { let mut children = Vec::new(); self.while_indented_lower(|parser| { if let Some(parsed_child) = parser.parse_child(|parser| Ok(Some(child(parser)?)))? { children.push(parsed_child); } Ok(()) })?; Ok(children) } fn parse_statements( &mut self, statement: fn(&mut Self) -> SassResult>, ) -> SassResult> { if self.toks.next_char_is(' ') || self.toks.next_char_is('\t') { return Err(( "Indenting at the beginning of the document is illegal.", self.toks.current_span(), ) .into()); } let mut statements = Vec::new(); while self.toks.peek().is_some() { if let Some(child) = self.parse_child(statement)? { statements.push(child); } let indentation = self.read_indentation()?; assert_eq!(indentation, 0); } Ok(statements) } fn parse_silent_comment(&mut self) -> SassResult { let start = self.toks.cursor(); self.expect_char('/')?; self.expect_char('/')?; let mut buffer = String::new(); let parent_indentation = self.current_indentation; 'outer: loop { let comment_prefix = if self.scan_char('/') { "///" } else { "//" }; loop { buffer.push_str(comment_prefix); // buffer.write(commentPrefix); // Skip the initial characters because we're already writing the // slashes. for _ in comment_prefix.len()..(self.current_indentation - parent_indentation) { buffer.push(' '); } while self.toks.peek().is_some() && !self.toks.next_char_is('\n') { buffer.push(self.toks.next().unwrap().kind); } buffer.push('\n'); if self.peek_indentation()? < parent_indentation { break 'outer; } if self.peek_indentation()? == parent_indentation { // Look ahead to the next line to see if it starts another comment. if matches!( self.toks.peek_n(1 + parent_indentation), Some(Token { kind: '/', .. }) ) && matches!( self.toks.peek_n(2 + parent_indentation), Some(Token { kind: '/', .. }) ) { self.read_indentation()?; } break; } self.read_indentation()?; } if !self.scan("//") { break; } } Ok(AstStmt::SilentComment(AstSilentComment { text: buffer, span: self.toks.span_from(start), })) } fn parse_loud_comment(&mut self) -> SassResult { let start = self.toks.cursor(); self.expect_char('/')?; self.expect_char('*')?; let mut first = true; let mut buffer = Interpolation::new_plain("/*".to_owned()); let parent_indentation = self.current_indentation; loop { if first { let beginning_of_comment = self.toks.cursor(); self.spaces(); if self.toks.next_char_is('\n') { self.read_indentation()?; buffer.add_char(' '); } else { buffer.add_string(self.toks.raw_text(beginning_of_comment)); } } else { buffer.add_string("\n * ".to_owned()); } first = false; for _ in 3..(self.current_indentation - parent_indentation) { buffer.add_char(' '); } while self.toks.peek().is_some() { match self.toks.peek() { Some(Token { kind: '\n' | '\r', .. }) => break, Some(Token { kind: '#', .. }) => { if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { buffer.add_char('#'); self.toks.next(); } } Some(Token { kind, .. }) => { buffer.add_char(kind); self.toks.next(); } None => todo!(), } } if self.peek_indentation()? <= parent_indentation { break; } // Preserve empty lines. while self.looking_at_double_newline() { self.expect_newline()?; buffer.add_char('\n'); buffer.add_char(' '); buffer.add_char('*'); } self.read_indentation()?; } if !buffer.trailing_string().trim_end().ends_with("*/") { buffer.add_string(" */".to_owned()); } Ok(AstLoudComment { text: buffer, span: self.toks.span_from(start), }) } } impl<'a> SassParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IS_USE_ALLOWED, true); SassParser { toks, path: file_name, empty_span, flags, options, current_indentation: 0, next_indentation: None, next_indentation_end: None, spaces: None, } } fn peek_indentation(&mut self) -> SassResult { if let Some(next) = self.next_indentation { return Ok(next); } if self.toks.peek().is_none() { self.next_indentation = Some(0); self.next_indentation_end = Some(self.toks.cursor()); return Ok(0); } let start = self.toks.cursor(); if !self.scan_char('\n') { return Err(("Expected newline.", self.toks.current_span()).into()); } let mut contains_tab; let mut contains_space; let mut next_indentation; loop { contains_tab = false; contains_space = false; next_indentation = 0; while let Some(next) = self.toks.peek() { match next.kind { ' ' => contains_space = true, '\t' => contains_tab = true, _ => break, } next_indentation += 1; self.toks.next(); } if self.toks.peek().is_none() { self.next_indentation = Some(0); self.next_indentation_end = Some(self.toks.cursor()); self.toks.set_cursor(start); return Ok(0); } if !self.scan_char('\n') { break; } } self.check_indentation_consistency(contains_tab, contains_space, start)?; self.next_indentation = Some(next_indentation); if next_indentation > 0 { self.spaces.get_or_insert(contains_space); } self.next_indentation_end = Some(self.toks.cursor()); self.toks.set_cursor(start); Ok(next_indentation) } fn check_indentation_consistency( &mut self, contains_tab: bool, contains_space: bool, start: usize, ) -> SassResult<()> { // NOTE: error message spans here start from the beginning of the line if contains_tab { if contains_space { return Err(( "Tabs and spaces may not be mixed.", self.toks.span_from(start), ) .into()); } else if self.spaces == Some(true) { return Err(("Expected spaces, was tabs.", self.toks.span_from(start)).into()); } } else if contains_space && self.spaces == Some(false) { return Err(("Expected tabs, was spaces.", self.toks.span_from(start)).into()); } Ok(()) } fn expect_newline(&mut self) -> SassResult<()> { match self.toks.peek() { Some(Token { kind: ';', .. }) => Err(( "semicolons aren't allowed in the indented syntax.", self.toks.current_span(), ) .into()), Some(Token { kind: '\r', .. }) => { self.toks.next(); self.scan_char('\n'); Ok(()) } Some(Token { kind: '\n', .. }) => { self.toks.next(); Ok(()) } _ => Err(("expected newline.", self.toks.current_span()).into()), } } fn read_indentation(&mut self) -> SassResult { self.current_indentation = match self.next_indentation { Some(indent) => indent, None => { let indent = self.peek_indentation()?; self.next_indentation = Some(indent); indent } }; self.toks.set_cursor(self.next_indentation_end.unwrap()); self.next_indentation = None; self.next_indentation_end = None; Ok(self.current_indentation) } fn while_indented_lower( &mut self, mut body: impl FnMut(&mut Self) -> SassResult<()>, ) -> SassResult<()> { let parent_indentation = self.current_indentation; let mut child_indentation = None; while self.peek_indentation()? > parent_indentation { let indentation = self.read_indentation()?; let child_indent = *child_indentation.get_or_insert(indentation); if child_indent != indentation { return Err(( format!( "Inconsistent indentation, expected {child_indent} spaces.", child_indent = child_indent ), self.toks.current_span(), ) .into()); } body(self)?; } Ok(()) } fn parse_child( &mut self, child: impl FnOnce(&mut Self) -> SassResult>, ) -> SassResult> { Ok(Some(match self.toks.peek() { Some(Token { kind: '\n' | '\r', .. }) => return Ok(None), Some(Token { kind: '$', .. }) => AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, ), Some(Token { kind: '/', .. }) => match self.toks.peek_n(1) { Some(Token { kind: '/', .. }) => self.parse_silent_comment()?, Some(Token { kind: '*', .. }) => AstStmt::LoudComment(self.parse_loud_comment()?), _ => return child(self), }, _ => return child(self), })) } fn looking_at_double_newline(&mut self) -> bool { match self.toks.peek() { // todo: is this branch reachable Some(Token { kind: '\r', .. }) => match self.toks.peek_n(1) { Some(Token { kind: '\n', .. }) => { matches!(self.toks.peek_n(2), Some(Token { kind: '\n', .. })) } Some(Token { kind: '\r', .. }) => true, _ => false, }, Some(Token { kind: '\n', .. }) => matches!( self.toks.peek_n(1), Some(Token { kind: '\n' | '\r', .. }) ), _ => false, } } } grass-0.13.4/crates/compiler/src/parse/scss.rs000066400000000000000000000026761465374720000212560ustar00rootroot00000000000000use std::path::Path; use codemap::Span; use crate::{lexer::Lexer, ContextFlags, Options}; use super::{BaseParser, StylesheetParser}; pub(crate) struct ScssParser<'a> { pub toks: Lexer, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, } impl<'a> ScssParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IS_USE_ALLOWED, true); ScssParser { toks, path: file_name, empty_span, flags, options, } } } impl<'a> BaseParser for ScssParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl<'a> StylesheetParser<'a> for ScssParser<'a> { fn is_plain_css(&self) -> bool { false } fn is_indented(&self) -> bool { false } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn current_indentation(&self) -> usize { 0 } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn empty_span(&self) -> Span { self.empty_span } } grass-0.13.4/crates/compiler/src/parse/stylesheet.rs000066400000000000000000003200221465374720000224600ustar00rootroot00000000000000use std::{ cell::Cell, collections::{BTreeMap, HashSet}, ffi::OsString, mem, path::{Path, PathBuf}, sync::Arc, }; use codemap::{Span, Spanned}; use crate::{ ast::*, common::{unvendor, Identifier, QuoteKind}, error::SassResult, lexer::Lexer, utils::{is_name, is_name_start, is_plain_css_import, opposite_bracket}, ContextFlags, Options, Token, }; use super::{ value::{Predicate, ValueParser}, BaseParser, DeclarationOrBuffer, ScssParser, VariableDeclOrInterpolation, RESERVED_IDENTIFIERS, }; /// Default implementations are oriented towards the SCSS syntax, as both CSS and /// SCSS share the behavior pub(crate) trait StylesheetParser<'a>: BaseParser + Sized { // todo: make constant? fn is_plain_css(&self) -> bool; // todo: make constant? fn is_indented(&self) -> bool; fn options(&self) -> &Options; fn path(&self) -> &Path; fn empty_span(&self) -> Span; fn current_indentation(&self) -> usize; fn flags(&self) -> &ContextFlags; fn flags_mut(&mut self) -> &mut ContextFlags; #[allow(clippy::type_complexity)] const IDENTIFIER_LIKE: Option SassResult>> = None; fn parse_style_rule_selector(&mut self) -> SassResult { self.almost_any_value(false) } fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> { self.whitespace_without_comments(); match self.toks().peek() { Some(Token { kind: ';' | '}', .. }) | None => Ok(()), _ => { self.expect_char(';')?; unreachable!(); } } } fn at_end_of_statement(&self) -> bool { matches!( self.toks().peek(), Some(Token { kind: ';' | '}' | '{', .. }) | None ) } fn looking_at_children(&mut self) -> SassResult { Ok(matches!(self.toks().peek(), Some(Token { kind: '{', .. }))) } fn scan_else(&mut self, _if_indentation: usize) -> SassResult { let start = self.toks().cursor(); self.whitespace()?; if self.scan_char('@') { if self.scan_identifier("else", true)? { return Ok(true); } if self.scan_identifier("elseif", true)? { // todo: deprecation warning here let new_cursor = self.toks().cursor() - 2; self.toks_mut().set_cursor(new_cursor); return Ok(true); } } self.toks_mut().set_cursor(start); Ok(false) } fn parse_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult> { self.expect_char('{')?; self.whitespace_without_comments(); let mut children = Vec::new(); let mut found_matching_brace = false; while let Some(tok) = self.toks().peek() { match tok.kind { '$' => children.push(AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, )), '/' => match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { children.push(self.parse_silent_comment()?); self.whitespace_without_comments(); } Some(Token { kind: '*', .. }) => { children.push(AstStmt::LoudComment(self.parse_loud_comment()?)); self.whitespace_without_comments(); } _ => children.push(child(self)?), }, ';' => { self.toks_mut().next(); self.whitespace_without_comments(); } '}' => { self.expect_char('}')?; found_matching_brace = true; break; } _ => children.push(child(self)?), } } if !found_matching_brace { return Err(("expected \"}\".", self.toks().current_span()).into()); } Ok(children) } fn parse_statements( &mut self, statement: fn(&mut Self) -> SassResult>, ) -> SassResult> { let mut stmts = Vec::new(); self.whitespace_without_comments(); while let Some(tok) = self.toks().peek() { match tok.kind { '$' => stmts.push(AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, )), '/' => match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { stmts.push(self.parse_silent_comment()?); self.whitespace_without_comments(); } Some(Token { kind: '*', .. }) => { stmts.push(AstStmt::LoudComment(self.parse_loud_comment()?)); self.whitespace_without_comments(); } _ => { if let Some(stmt) = statement(self)? { stmts.push(stmt); } } }, ';' => { self.toks_mut().next(); self.whitespace_without_comments(); } _ => { if let Some(stmt) = statement(self)? { stmts.push(stmt); } } } } Ok(stmts) } // todo: rename fn __parse(&mut self) -> SassResult { let mut style_sheet = StyleSheet::new( self.is_plain_css(), self.options() .fs .canonicalize(self.path()) .unwrap_or_else(|_| self.path().to_path_buf()), ); // Allow a byte-order mark at the beginning of the document. self.scan_char('\u{feff}'); style_sheet.body = self.parse_statements(|parser| { if parser.next_matches("@charset") { parser.expect_char('@')?; parser.expect_identifier("charset", false)?; parser.whitespace()?; parser.parse_string()?; return Ok(None); } Ok(Some(parser.parse_statement()?)) })?; for (idx, child) in style_sheet.body.iter().enumerate() { match child { AstStmt::VariableDecl(_) | AstStmt::LoudComment(_) | AstStmt::SilentComment(_) => { continue } AstStmt::Use(..) => style_sheet.uses.push(idx), AstStmt::Forward(..) => style_sheet.forwards.push(idx), _ => break, } } Ok(style_sheet) } fn looking_at_expression(&mut self) -> bool { let character = if let Some(c) = self.toks().peek() { c } else { return false; }; match character.kind { '.' => !matches!(self.toks().peek_n(1), Some(Token { kind: '.', .. })), '!' => match self.toks().peek_n(1) { Some(Token { kind: 'i' | 'I', .. }) | None => true, Some(Token { kind, .. }) => kind.is_ascii_whitespace(), }, '(' | '/' | '[' | '\'' | '"' | '#' | '+' | '-' | '\\' | '$' | '&' => true, c => is_name_start(c) || c.is_ascii_digit(), } } fn parse_argument_declaration(&mut self) -> SassResult { self.expect_char('(')?; self.whitespace()?; let mut arguments = Vec::new(); let mut named = HashSet::new(); let mut rest_argument: Option = None; while self.toks_mut().next_char_is('$') { let name_start = self.toks().cursor(); let name = Identifier::from(self.parse_variable_name()?); let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let mut default_value: Option = None; if self.scan_char(':') { self.whitespace()?; default_value = Some(self.parse_expression_until_comma(false)?.node); } else if self.scan_char('.') { self.expect_char('.')?; self.expect_char('.')?; self.whitespace()?; rest_argument = Some(name); break; } arguments.push(Argument { name, default: default_value, }); if !named.insert(name) { return Err(("Duplicate argument.", name_span).into()); } if !self.scan_char(',') { break; } self.whitespace()?; } self.expect_char(')')?; Ok(ArgumentDeclaration { args: arguments, rest: rest_argument, }) } fn plain_at_rule_name(&mut self) -> SassResult { self.expect_char('@')?; let name = self.parse_identifier(false, false)?; self.whitespace()?; Ok(name) } fn with_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult>> { let start = self.toks().cursor(); let children = self.parse_children(child)?; let span = self.toks_mut().span_from(start); self.whitespace_without_comments(); Ok(Spanned { node: children, span, }) } fn parse_at_root_query(&mut self) -> SassResult { let mut buffer = Interpolation::new(); self.expect_char('(')?; buffer.add_char('('); self.whitespace()?; buffer.add_expr(self.parse_expression(None, None, None)?); if self.scan_char(':') { self.whitespace()?; buffer.add_char(':'); buffer.add_char(' '); buffer.add_expr(self.parse_expression(None, None, None)?); } self.expect_char(')')?; self.whitespace()?; buffer.add_char(')'); Ok(buffer) } fn parse_at_root_rule(&mut self, start: usize) -> SassResult { Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') { let query_start = self.toks().cursor(); let query = self.parse_at_root_query()?; let query_span = self.toks_mut().span_from(query_start); self.whitespace()?; let children = self.with_children(Self::parse_statement)?.node; AstAtRootRule { query: Some(Spanned { node: query, span: query_span, }), body: children, span: self.toks_mut().span_from(start), } } else if self.looking_at_children()? { let children = self.with_children(Self::parse_statement)?.node; AstAtRootRule { query: None, body: children, span: self.toks_mut().span_from(start), } } else { let child = self.parse_style_rule(None, None)?; AstAtRootRule { query: None, body: vec![child], span: self.toks_mut().span_from(start), } })) } fn parse_content_rule(&mut self, start: usize) -> SassResult { if !self.flags().in_mixin() { return Err(( "@content is only allowed within mixin declarations.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_invocation(true, false)? } else { ArgumentInvocation::empty(self.toks().current_span()) }; self.expect_statement_separator(Some("@content rule"))?; self.flags_mut().set(ContextFlags::FOUND_CONTENT_RULE, true); Ok(AstStmt::ContentRule(AstContentRule { args })) } fn parse_debug_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@debug rule"))?; Ok(AstStmt::Debug(AstDebugRule { value: value.node, span: value.span, })) } fn parse_each_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let mut variables = vec![Identifier::from(self.parse_variable_name()?)]; self.whitespace()?; while self.scan_char(',') { self.whitespace()?; variables.push(Identifier::from(self.parse_variable_name()?)); self.whitespace()?; } self.expect_identifier("in", false)?; self.whitespace()?; let list = self.parse_expression(None, None, None)?.node; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::Each(AstEach { variables, list, body, })) } fn parse_disallowed_at_rule(&mut self, start: usize) -> SassResult { self.almost_any_value(false)?; Err(( "This at-rule is not allowed here.", self.toks_mut().span_from(start), ) .into()) } fn parse_error_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@error rule"))?; Ok(AstStmt::ErrorRule(AstErrorRule { value: value.node, span: value.span, })) } fn parse_extend_rule(&mut self, start: usize) -> SassResult { if !self.flags().in_style_rule() && !self.flags().in_mixin() && !self.flags().in_content_block() { return Err(( "@extend may only be used within style rules.", self.toks_mut().span_from(start), ) .into()); } let value = self.almost_any_value(false)?; let is_optional = self.scan_char('!'); if is_optional { self.expect_identifier("optional", false)?; } self.expect_statement_separator(Some("@extend rule"))?; Ok(AstStmt::Extend(AstExtendRule { value, is_optional, span: self.toks_mut().span_from(start), })) } fn parse_for_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let var_start = self.toks().cursor(); let variable = Spanned { node: Identifier::from(self.parse_variable_name()?), span: self.toks_mut().span_from(var_start), }; self.whitespace()?; self.expect_identifier("from", false)?; self.whitespace()?; let exclusive: Cell> = Cell::new(None); let from = self.parse_expression( Some(&|parser| { if !parser.looking_at_identifier() { return Ok(false); } Ok(if parser.scan_identifier("to", false)? { exclusive.set(Some(true)); true } else if parser.scan_identifier("through", false)? { exclusive.set(Some(false)); true } else { false }) }), None, None, )?; let is_exclusive = match exclusive.get() { Some(b) => b, None => { return Err(( "Expected \"to\" or \"through\".", self.toks().current_span(), ) .into()) } }; self.whitespace()?; let to = self.parse_expression(None, None, None)?; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::For(AstFor { variable, from, to, is_exclusive, body, })) } fn parse_function_rule(&mut self, start: usize) -> SassResult { let name_start = self.toks().cursor(); let name = self.parse_identifier(true, false)?; let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let arguments = self.parse_argument_declaration()?; if self.flags().in_mixin() || self.flags().in_content_block() { return Err(( "Mixins may not contain function declarations.", self.toks_mut().span_from(start), ) .into()); } else if self.flags().in_control_flow() { return Err(( "Functions may not be declared in control directives.", self.toks_mut().span_from(start), ) .into()); } if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) { return Err(("Invalid function name.", self.toks_mut().span_from(start)).into()); } self.whitespace()?; let children = self.with_children(Self::function_child)?.node; Ok(AstStmt::FunctionDecl(AstFunctionDecl { name: Spanned { node: Identifier::from(name), span: name_span, }, arguments, body: children, })) } fn parse_variable_declaration_with_namespace(&mut self) -> SassResult { let start = self.toks().cursor(); let namespace = self.parse_identifier(false, false)?; let namespace_span = self.toks_mut().span_from(start); self.expect_char('.')?; self.parse_variable_declaration_without_namespace( Some(Spanned { node: Identifier::from(namespace), span: namespace_span, }), Some(start), ) } fn function_child(&mut self) -> SassResult { let start = self.toks().cursor(); if !self.toks_mut().next_char_is('@') { match self.parse_variable_declaration_with_namespace() { Ok(decl) => return Ok(AstStmt::VariableDecl(decl)), Err(e) => { self.toks_mut().set_cursor(start); let stmt = match self.parse_declaration_or_style_rule() { Ok(stmt) => stmt, Err(..) => return Err(e), }; let (is_style_rule, span) = match stmt { AstStmt::RuleSet(ruleset) => (true, ruleset.span), AstStmt::Style(style) => (false, style.span), _ => unreachable!(), }; return Err(( format!( "@function rules may not contain {}.", if is_style_rule { "style rules" } else { "declarations" } ), span, ) .into()); } } } return match self.plain_at_rule_name()?.as_str() { "debug" => self.parse_debug_rule(), "each" => self.parse_each_rule(Self::function_child), "else" => self.parse_disallowed_at_rule(start), "error" => self.parse_error_rule(), "for" => self.parse_for_rule(Self::function_child), "if" => self.parse_if_rule(Self::function_child), "return" => self.parse_return_rule(), "warn" => self.parse_warn_rule(), "while" => self.parse_while_rule(Self::function_child), _ => self.parse_disallowed_at_rule(start), }; } fn parse_if_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let if_indentation = self.current_indentation(); let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let condition = self.parse_expression(None, None, None)?.node; let body = self.parse_children(child)?; self.whitespace_without_comments(); let mut clauses = vec![AstIfClause { condition, body }]; let mut last_clause: Option> = None; while self.scan_else(if_indentation)? { self.whitespace()?; if self.scan_identifier("if", false)? { self.whitespace()?; let condition = self.parse_expression(None, None, None)?.node; let body = self.parse_children(child)?; clauses.push(AstIfClause { condition, body }); } else { last_clause = Some(self.parse_children(child)?); break; } } self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); self.whitespace_without_comments(); Ok(AstStmt::If(AstIf { if_clauses: clauses, else_clause: last_clause, })) } fn try_parse_import_supports_function(&mut self) -> SassResult> { if !self.looking_at_interpolated_identifier() { return Ok(None); } let start = self.toks().cursor(); let name = self.parse_interpolated_identifier()?; debug_assert!(name.as_plain() != Some("not")); if !self.scan_char('(') { self.toks_mut().set_cursor(start); return Ok(None); } let value = self.parse_interpolated_declaration_value(true, true, true)?; self.expect_char(')')?; Ok(Some(AstSupportsCondition::Function { name, args: value })) } fn parse_import_supports_query(&mut self) -> SassResult { Ok(if self.scan_identifier("not", false)? { self.whitespace()?; AstSupportsCondition::Negation(Box::new(self.supports_condition_in_parens()?)) } else if self.toks_mut().next_char_is('(') { self.parse_supports_condition()? } else { match self.try_parse_import_supports_function()? { Some(function) => function, None => { let start = self.toks().cursor(); let name = self.parse_expression(None, None, None)?; self.expect_char(':')?; self.supports_declaration_value(name.node, start)? } } }) } fn try_import_modifiers(&mut self) -> SassResult> { // Exit before allocating anything if we're not looking at any modifiers, as // is the most common case. if !self.looking_at_interpolated_identifier() && !self.toks_mut().next_char_is('(') { return Ok(None); } let mut buffer = Interpolation::new(); loop { if self.looking_at_interpolated_identifier() { if !buffer.is_empty() { buffer.add_char(' '); } let identifier = self.parse_interpolated_identifier()?; let name = identifier.as_plain().map(str::to_ascii_lowercase); buffer.add_interpolation(identifier); if name.as_deref() != Some("and") && self.scan_char('(') { if name.as_deref() == Some("supports") { let query = self.parse_import_supports_query()?; let is_declaration = matches!(query, AstSupportsCondition::Declaration { .. }); if !is_declaration { buffer.add_char('('); } buffer.add_expr(AstExpr::Supports(Arc::new(query)).span(self.empty_span())); if !is_declaration { buffer.add_char(')'); } } else { buffer.add_char('('); buffer.add_interpolation( self.parse_interpolated_declaration_value(true, true, true)?, ); buffer.add_char(')'); } self.expect_char(')')?; self.whitespace()?; } else { self.whitespace()?; if self.scan_char(',') { buffer.add_char(','); buffer.add_char(' '); buffer.add_interpolation(self.parse_media_query_list()?); return Ok(Some(buffer)); } } } else if self.toks_mut().next_char_is('(') { if !buffer.is_empty() { buffer.add_char(' '); } buffer.add_interpolation(self.parse_media_query_list()?); return Ok(Some(buffer)); } else { return Ok(Some(buffer)); } } } fn try_url_contents(&mut self, name: Option<&str>) -> SassResult> { let start = self.toks().cursor(); if !self.scan_char('(') { return Ok(None); } self.whitespace_without_comments(); // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. let mut buffer = Interpolation::new(); buffer.add_string(name.unwrap_or("url").to_owned()); buffer.add_char('('); while let Some(next) = self.toks().peek() { match next.kind { '\\' => buffer.add_string(self.parse_escape(false)?), '!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { self.toks_mut().next(); buffer.add_char(next.kind); } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { let interpolation = self.parse_single_interpolation()?; buffer.add_interpolation(interpolation); } else { self.toks_mut().next(); buffer.add_char(next.kind); } } ')' => { self.toks_mut().next(); buffer.add_char(next.kind); return Ok(Some(buffer)); } ' ' | '\t' | '\n' | '\r' => { self.whitespace_without_comments(); if !self.toks_mut().next_char_is(')') { break; } } _ => break, } } self.toks_mut().set_cursor(start); Ok(None) } fn parse_dynamic_url(&mut self) -> SassResult { let start = self.toks().cursor(); self.expect_identifier("url", false)?; Ok(match self.try_url_contents(None)? { Some(contents) => AstExpr::String( StringExpr(contents, QuoteKind::None), self.toks_mut().span_from(start), ), None => AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: Interpolation::new_plain("url".to_owned()), arguments: self.parse_argument_invocation(false, false)?, span: self.toks_mut().span_from(start), })), }) } fn parse_import_argument(&mut self, start: usize) -> SassResult { if self.toks_mut().next_char_is('u') || self.toks_mut().next_char_is('U') { let url = self.parse_dynamic_url()?; self.whitespace()?; let modifiers = self.try_import_modifiers()?; return Ok(AstImport::Plain(AstPlainCssImport { url: Interpolation::new_with_expr(url.span(self.toks_mut().span_from(start))), modifiers, span: self.toks_mut().span_from(start), })); } let start = self.toks().cursor(); let url = self.parse_string()?; let raw_url = self.toks().raw_text(start); self.whitespace()?; let modifiers = self.try_import_modifiers()?; let span = self.toks_mut().span_from(start); if is_plain_css_import(&url) || modifiers.is_some() { Ok(AstImport::Plain(AstPlainCssImport { url: Interpolation::new_plain(raw_url), modifiers, span, })) } else { // todo: try parseImportUrl Ok(AstImport::Sass(AstSassImport { url, span })) } } fn parse_import_rule(&mut self, start: usize) -> SassResult { let mut imports = Vec::new(); loop { self.whitespace()?; let argument = self.parse_import_argument(self.toks().cursor())?; // todo: _inControlDirective if (self.flags().in_control_flow() || self.flags().in_mixin()) && argument.is_dynamic() { self.parse_disallowed_at_rule(start)?; } imports.push(argument); self.whitespace()?; if !self.scan_char(',') { break; } } Ok(AstStmt::ImportRule(AstImportRule { imports })) } fn parse_public_identifier(&mut self) -> SassResult { let start = self.toks().cursor(); let ident = self.parse_identifier(true, false)?; Self::assert_public(&ident, self.toks_mut().span_from(start))?; Ok(ident) } fn parse_include_rule(&mut self) -> SassResult { let mut namespace: Option> = None; let name_start = self.toks().cursor(); let mut name = self.parse_identifier(false, false)?; if self.scan_char('.') { let namespace_span = self.toks_mut().span_from(name_start); namespace = Some(Spanned { node: Identifier::from(name), span: namespace_span, }); name = self.parse_public_identifier()?; } else { name = name.replace('_', "-"); } let name = Identifier::from(name); let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_invocation(true, false)? } else { ArgumentInvocation::empty(self.toks().current_span()) }; self.whitespace()?; let content_args = if self.scan_identifier("using", false)? { self.whitespace()?; let args = self.parse_argument_declaration()?; self.whitespace()?; Some(args) } else { None }; let mut content_block: Option = None; if content_args.is_some() || self.looking_at_children()? { let content_args = content_args.unwrap_or_else(ArgumentDeclaration::empty); let was_in_content_block = self.flags().in_content_block(); self.flags_mut().set(ContextFlags::IN_CONTENT_BLOCK, true); let body = self.with_children(Self::parse_statement)?.node; content_block = Some(AstContentBlock { args: content_args, body, }); self.flags_mut() .set(ContextFlags::IN_CONTENT_BLOCK, was_in_content_block); } else { self.expect_statement_separator(None)?; } Ok(AstStmt::Include(AstInclude { namespace, name: Spanned { node: name, span: name_span, }, args, content: content_block, span: name_span, })) } fn parse_media_rule(&mut self, start: usize) -> SassResult { let query_start = self.toks().cursor(); let query = self.parse_media_query_list()?; let query_span = self.toks_mut().span_from(query_start); let body = self.with_children(Self::parse_statement)?.node; Ok(AstStmt::Media(AstMedia { query, query_span, body, span: self.toks_mut().span_from(start), })) } fn parse_interpolated_string(&mut self) -> SassResult> { let start = self.toks().cursor(); let quote = match self.toks_mut().next() { Some(Token { kind: kind @ ('"' | '\''), .. }) => kind, Some(..) | None => unreachable!("Expected string."), }; let mut buffer = Interpolation::new(); let mut found_match = false; while let Some(next) = self.toks().peek() { match next.kind { c if c == quote => { self.toks_mut().next(); found_match = true; break; } '\n' => break, '\\' => { match self.toks().peek_n(1) { // todo: if (second == $cr) scanner.scanChar($lf); // we basically need to stop normalizing to gain parity Some(Token { kind: '\n', .. }) => { self.toks_mut().next(); self.toks_mut().next(); } _ => buffer.add_char(self.consume_escaped_char()?), } } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { self.toks_mut().next(); buffer.add_char(next.kind); } } _ => { buffer.add_char(next.kind); self.toks_mut().next(); } } } if !found_match { return Err(( format!("Expected {quote}.", quote = quote), self.toks().current_span(), ) .into()); } Ok(Spanned { node: StringExpr(buffer, QuoteKind::Quoted), span: self.toks_mut().span_from(start), }) } fn parse_return_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(None)?; Ok(AstStmt::Return(AstReturn { val: value.node, span: value.span, })) } fn parse_mixin_rule(&mut self, start: usize) -> SassResult { let name = Identifier::from(self.parse_identifier(true, false)?); self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_declaration()? } else { ArgumentDeclaration::empty() }; if self.flags().in_mixin() || self.flags().in_content_block() { return Err(( "Mixins may not contain mixin declarations.", self.toks_mut().span_from(start), ) .into()); } else if self.flags().in_control_flow() { return Err(( "Mixins may not be declared in control directives.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; let old_found_content_rule = self.flags().found_content_rule(); self.flags_mut() .set(ContextFlags::FOUND_CONTENT_RULE, false); self.flags_mut().set(ContextFlags::IN_MIXIN, true); let body = self.with_children(Self::parse_statement)?.node; let has_content = self.flags_mut().found_content_rule(); self.flags_mut() .set(ContextFlags::FOUND_CONTENT_RULE, old_found_content_rule); self.flags_mut().set(ContextFlags::IN_MIXIN, false); Ok(AstStmt::Mixin(AstMixin { name, args, body, has_content, })) } fn _parse_moz_document_rule(&mut self, _name: Interpolation) -> SassResult { todo!("special cased @-moz-document not yet implemented") } fn unknown_at_rule(&mut self, name: Interpolation, start: usize) -> SassResult { let was_in_unknown_at_rule = self.flags().in_unknown_at_rule(); self.flags_mut().set(ContextFlags::IN_UNKNOWN_AT_RULE, true); let value: Option = if !self.toks_mut().next_char_is('!') && !self.at_end_of_statement() { Some(self.almost_any_value(false)?) } else { None }; let children = if self.looking_at_children()? { Some(self.with_children(Self::parse_statement)?.node) } else { self.expect_statement_separator(None)?; None }; self.flags_mut() .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); Ok(AstStmt::UnknownAtRule(AstUnknownAtRule { name, value, body: children, span: self.toks_mut().span_from(start), })) } fn try_supports_operation( &mut self, interpolation: &Interpolation, _start: usize, ) -> SassResult> { if interpolation.contents.len() != 1 { return Ok(None); } let expression = match interpolation.contents.first() { Some(InterpolationPart::Expr(e)) => e, Some(InterpolationPart::String(..)) => return Ok(None), None => unreachable!(), }; let before_whitespace = self.toks().cursor(); self.whitespace()?; let mut operation: Option = None; let mut operator: Option = None; while self.looking_at_identifier() { if let Some(operator) = &operator { self.expect_identifier(operator, false)?; } else if self.scan_identifier("and", false)? { operator = Some("and".to_owned()); } else if self.scan_identifier("or", false)? { operator = Some("or".to_owned()); } else { self.toks_mut().set_cursor(before_whitespace); return Ok(None); } self.whitespace()?; let right = self.supports_condition_in_parens()?; operation = Some(AstSupportsCondition::Operation { left: Box::new(operation.unwrap_or_else(|| { AstSupportsCondition::Interpolation(expression.clone().node) })), operator: operator.clone(), right: Box::new(right), }); self.whitespace()?; } Ok(operation) } fn supports_declaration_value( &mut self, name: AstExpr, start: usize, ) -> SassResult { let value = match &name { AstExpr::String(StringExpr(text, QuoteKind::None), ..) if text.initial_plain().starts_with("--") => { let text = self.parse_interpolated_declaration_value(false, false, true)?; AstExpr::String( StringExpr(text, QuoteKind::None), self.toks_mut().span_from(start), ) } _ => { self.whitespace()?; self.parse_expression(None, None, None)?.node } }; Ok(AstSupportsCondition::Declaration { name, value }) } fn supports_condition_in_parens(&mut self) -> SassResult { let start = self.toks().cursor(); if self.looking_at_interpolated_identifier() { let identifier = self.parse_interpolated_identifier()?; let ident_span = self.toks_mut().span_from(start); if identifier.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { return Err((r#""not" is not a valid identifier here."#, ident_span).into()); } if self.scan_char('(') { let arguments = self.parse_interpolated_declaration_value(true, true, true)?; self.expect_char(')')?; return Ok(AstSupportsCondition::Function { name: identifier, args: arguments, }); } else if identifier.contents.len() != 1 || !matches!( identifier.contents.first(), Some(InterpolationPart::Expr(..)) ) { return Err(("Expected @supports condition.", ident_span).into()); } else { match identifier.contents.first() { Some(InterpolationPart::Expr(e)) => { return Ok(AstSupportsCondition::Interpolation(e.clone().node)) } _ => unreachable!(), } } } self.expect_char('(')?; self.whitespace()?; if self.scan_identifier("not", false)? { self.whitespace()?; let condition = self.supports_condition_in_parens()?; self.expect_char(')')?; return Ok(AstSupportsCondition::Negation(Box::new(condition))); } else if self.toks_mut().next_char_is('(') { let condition = self.parse_supports_condition()?; self.expect_char(')')?; return Ok(condition); } // Unfortunately, we may have to backtrack here. The grammar is: // // Expression ":" Expression // | InterpolatedIdentifier InterpolatedAnyValue? // // These aren't ambiguous because this `InterpolatedAnyValue` is forbidden // from containing a top-level colon, but we still have to parse the full // expression to figure out if there's a colon after it. // // We could avoid the overhead of a full expression parse by looking ahead // for a colon (outside of balanced brackets), but in practice we expect the // vast majority of real uses to be `Expression ":" Expression`, so it makes // sense to parse that case faster in exchange for less code complexity and // a slower backtracking case. let name: AstExpr; let name_start = self.toks().cursor(); let was_in_parens = self.flags().in_parens(); let expr = self.parse_expression(None, None, None); let found_colon = self.expect_char(':'); match (expr, found_colon) { (Ok(val), Ok(..)) => { name = val.node; } (Ok(..), Err(e)) | (Err(e), Ok(..)) | (Err(e), Err(..)) => { self.toks_mut().set_cursor(name_start); self.flags_mut().set(ContextFlags::IN_PARENS, was_in_parens); let identifier = self.parse_interpolated_identifier()?; // todo: superfluous clone? if let Some(operation) = self.try_supports_operation(&identifier, name_start)? { self.expect_char(')')?; return Ok(operation); } // If parsing an expression fails, try to parse an // `InterpolatedAnyValue` instead. But if that value runs into a // top-level colon, then this is probably intended to be a declaration // after all, so we rethrow the declaration-parsing error. let mut contents = Interpolation::new(); contents.add_interpolation(identifier); contents.add_interpolation( self.parse_interpolated_declaration_value(true, true, false)?, ); if self.toks_mut().next_char_is(':') { return Err(e); } self.expect_char(')')?; return Ok(AstSupportsCondition::Anything { contents }); } } let declaration = self.supports_declaration_value(name, start)?; self.expect_char(')')?; Ok(declaration) } fn parse_supports_condition(&mut self) -> SassResult { if self.scan_identifier("not", false)? { self.whitespace()?; return Ok(AstSupportsCondition::Negation(Box::new( self.supports_condition_in_parens()?, ))); } let mut condition = self.supports_condition_in_parens()?; self.whitespace()?; let mut operator: Option = None; while self.looking_at_identifier() { if let Some(operator) = &operator { self.expect_identifier(operator, false)?; } else if self.scan_identifier("or", false)? { operator = Some("or".to_owned()); } else { self.expect_identifier("and", false)?; operator = Some("and".to_owned()); } self.whitespace()?; let right = self.supports_condition_in_parens()?; condition = AstSupportsCondition::Operation { left: Box::new(condition), operator: operator.clone(), right: Box::new(right), }; self.whitespace()?; } Ok(condition) } fn parse_supports_rule(&mut self) -> SassResult { let condition = self.parse_supports_condition()?; self.whitespace()?; let children = self.with_children(Self::parse_statement)?; Ok(AstStmt::Supports(AstSupportsRule { condition, body: children.node, span: children.span, })) } fn parse_warn_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@warn rule"))?; Ok(AstStmt::Warn(AstWarn { value: value.node, span: value.span, })) } fn parse_while_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let condition = self.parse_expression(None, None, None)?.node; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::While(AstWhile { condition, body })) } fn parse_forward_rule(&mut self, start: usize) -> SassResult { let url = PathBuf::from(self.parse_url_string()?); self.whitespace()?; let prefix = if self.scan_identifier("as", false)? { self.whitespace()?; let prefix = self.parse_identifier(true, false)?; self.expect_char('*')?; self.whitespace()?; Some(prefix) } else { None }; let mut shown_mixins_and_functions: Option> = None; let mut shown_variables: Option> = None; let mut hidden_mixins_and_functions: Option> = None; let mut hidden_variables: Option> = None; if self.scan_identifier("show", false)? { let members = self.parse_member_list()?; shown_mixins_and_functions = Some(members.0); shown_variables = Some(members.1); } else if self.scan_identifier("hide", false)? { let members = self.parse_member_list()?; hidden_mixins_and_functions = Some(members.0); hidden_variables = Some(members.1); } let config = self.parse_configuration(true)?; self.expect_statement_separator(Some("@forward rule"))?; let span = self.toks_mut().span_from(start); if !self.flags().is_use_allowed() { return Err(( "@forward rules must be written before any other rules.", span, ) .into()); } Ok(AstStmt::Forward( if let (Some(shown_mixins_and_functions), Some(shown_variables)) = (shown_mixins_and_functions, shown_variables) { AstForwardRule::show( url, shown_mixins_and_functions, shown_variables, prefix, config, span, ) } else if let (Some(hidden_mixins_and_functions), Some(hidden_variables)) = (hidden_mixins_and_functions, hidden_variables) { AstForwardRule::hide( url, hidden_mixins_and_functions, hidden_variables, prefix, config, span, ) } else { AstForwardRule::new(url, prefix, config, span) }, )) } fn parse_member_list(&mut self) -> SassResult<(HashSet, HashSet)> { let mut identifiers = HashSet::new(); let mut variables = HashSet::new(); loop { self.whitespace()?; // todo: withErrorMessage("Expected variable, mixin, or function name" if self.toks_mut().next_char_is('$') { variables.insert(Identifier::from(self.parse_variable_name()?)); } else { identifiers.insert(Identifier::from(self.parse_identifier(true, false)?)); } self.whitespace()?; if !self.scan_char(',') { break; } } Ok((identifiers, variables)) } fn parse_url_string(&mut self) -> SassResult { // todo: real uri parsing self.parse_string() } fn use_namespace( &mut self, url: &Path, _start: usize, url_span: Span, ) -> SassResult> { if self.scan_identifier("as", false)? { self.whitespace()?; return Ok(if self.scan_char('*') { None } else { Some(self.parse_identifier(false, false)?) }); } let base_name = url .file_name() .map_or_else(OsString::new, ToOwned::to_owned); let base_name = base_name.to_string_lossy(); let dot = base_name.find('.'); let start = if base_name.starts_with('_') { 1 } else { 0 }; let end = dot.unwrap_or(base_name.len()); let namespace = if url.to_string_lossy().starts_with("sass:") { return Ok(Some(url.to_string_lossy().into_owned())); } else { &base_name[start..end] }; let mut toks = Lexer::new_from_string(namespace, url_span); // if namespace is empty, avoid attempting to parse an identifier from // an empty string, as there will be no span to emit let identifier = if namespace.is_empty() { Err(("", self.empty_span()).into()) } else { mem::swap(self.toks_mut(), &mut toks); let ident = self.parse_identifier(false, false); mem::swap(self.toks_mut(), &mut toks); ident }; match (identifier, toks.peek().is_none()) { (Ok(i), true) => Ok(Some(i)), _ => { Err(( format!( "The default namespace \"{namespace}\" is not a valid Sass identifier.\n\nRecommendation: add an \"as\" clause to define an explicit namespace.", namespace = namespace ), self.toks_mut().span_from(start) ).into()) } } } fn parse_configuration( &mut self, // default=false allow_guarded: bool, ) -> SassResult>> { if !self.scan_identifier("with", false)? { return Ok(None); } let mut variable_names = HashSet::new(); let mut configuration = Vec::new(); self.whitespace()?; self.expect_char('(')?; loop { self.whitespace()?; let var_start = self.toks().cursor(); let name = Identifier::from(self.parse_variable_name()?); let name_span = self.toks_mut().span_from(var_start); self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let expr = self.parse_expression_until_comma(false)?; let mut is_guarded = false; let flag_start = self.toks().cursor(); if allow_guarded && self.scan_char('!') { let flag = self.parse_identifier(false, false)?; if flag == "default" { is_guarded = true; self.whitespace()?; } else { return Err( ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), ); } } let span = self.toks_mut().span_from(var_start); if variable_names.contains(&name) { return Err(("The same variable may only be configured once.", span).into()); } variable_names.insert(name); configuration.push(ConfiguredVariable { name: Spanned { node: name, span: name_span, }, expr, is_guarded, }); if !self.scan_char(',') { break; } self.whitespace()?; if !self.looking_at_expression() { break; } } self.expect_char(')')?; Ok(Some(configuration)) } fn parse_use_rule(&mut self, start: usize) -> SassResult { let url_start = self.toks().cursor(); let url = self.parse_url_string()?; let url_span = self.toks().span_from(url_start); self.whitespace()?; let path = PathBuf::from(url); let namespace = self.use_namespace(path.as_ref(), start, url_span)?; self.whitespace()?; let configuration = self.parse_configuration(false)?; self.expect_statement_separator(Some("@use rule"))?; let span = self.toks_mut().span_from(start); if !self.flags().is_use_allowed() { return Err(( "@use rules must be written before any other rules.", self.toks_mut().span_from(start), ) .into()); } self.expect_statement_separator(Some("@use rule"))?; Ok(AstStmt::Use(AstUseRule { url: path, namespace, configuration: configuration.unwrap_or_default(), span, })) } fn parse_at_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks().cursor(); self.expect_char('@')?; let name = self.parse_interpolated_identifier()?; self.whitespace()?; // We want to set [_isUseAllowed] to `false` *unless* we're parsing // `@charset`, `@forward`, or `@use`. To avoid double-comparing the rule // name, we always set it to `false` and then set it back to its previous // value if we're parsing an allowed rule. let was_use_allowed = self.flags().is_use_allowed(); self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); match name.as_plain() { Some("at-root") => self.parse_at_root_rule(start), Some("content") => self.parse_content_rule(start), Some("debug") => self.parse_debug_rule(), Some("each") => self.parse_each_rule(child), Some("else") | Some("return") => self.parse_disallowed_at_rule(start), Some("error") => self.parse_error_rule(), Some("extend") => self.parse_extend_rule(start), Some("for") => self.parse_for_rule(child), Some("forward") => { self.flags_mut() .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); // if (!root) { // _disallowedAtRule(); // } self.parse_forward_rule(start) } Some("function") => self.parse_function_rule(start), Some("if") => self.parse_if_rule(child), Some("import") => self.parse_import_rule(start), Some("include") => self.parse_include_rule(), Some("media") => self.parse_media_rule(start), Some("mixin") => self.parse_mixin_rule(start), // todo: support -moz-document // Some("-moz-document") => self.parse_moz_document_rule(name), Some("supports") => self.parse_supports_rule(), Some("use") => { self.flags_mut() .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); // if (!root) { // _disallowedAtRule(); // } self.parse_use_rule(start) } Some("warn") => self.parse_warn_rule(), Some("while") => self.parse_while_rule(child), Some(..) | None => self.unknown_at_rule(name, start), } } fn parse_statement(&mut self) -> SassResult { match self.toks().peek() { Some(Token { kind: '@', .. }) => self.parse_at_rule(Self::parse_statement), Some(Token { kind: '+', .. }) => { if !self.is_indented() { return self.parse_style_rule(None, None); } let start = self.toks().cursor(); self.toks_mut().next(); if !self.looking_at_identifier() { self.toks_mut().set_cursor(start); return self.parse_style_rule(None, None); } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); self.parse_include_rule() } Some(Token { kind: '=', .. }) => { if !self.is_indented() { return self.parse_style_rule(None, None); } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); let start = self.toks().cursor(); self.toks_mut().next(); self.whitespace()?; self.parse_mixin_rule(start) } Some(Token { kind: '}', .. }) => { Err(("unmatched \"}\".", self.toks().current_span()).into()) } _ => { if self.flags().in_style_rule() || self.flags().in_unknown_at_rule() || self.flags().in_mixin() || self.flags().in_content_block() { self.parse_declaration_or_style_rule() } else { self.parse_variable_declaration_or_style_rule() } } } } fn parse_declaration_or_style_rule(&mut self) -> SassResult { let start = self.toks().cursor(); if self.is_plain_css() && self.flags().in_style_rule() && !self.flags().in_unknown_at_rule() { return self.parse_property_or_variable_declaration(true); } // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. if self.is_indented() && self.scan_char('\\') { return self.parse_style_rule(None, None); }; match self.parse_declaration_or_buffer()? { DeclarationOrBuffer::Stmt(s) => Ok(s), DeclarationOrBuffer::Buffer(existing_buffer) => { self.parse_style_rule(Some(existing_buffer), Some(start)) } } } fn parse_property_or_variable_declaration( &mut self, // default=true parse_custom_properties: bool, ) -> SassResult { let start = self.toks().cursor(); let name = if matches!( self.toks().peek(), Some(Token { kind: ':' | '*' | '.', .. }) ) || (matches!(self.toks().peek(), Some(Token { kind: '#', .. })) && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) { // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" // hacks. let mut name_buffer = Interpolation::new(); name_buffer.add_char(self.toks_mut().next().unwrap().kind); name_buffer.add_string(self.raw_text(Self::whitespace)); name_buffer.add_interpolation(self.parse_interpolated_identifier()?); name_buffer } else if !self.is_plain_css() { match self.parse_variable_declaration_or_interpolation()? { VariableDeclOrInterpolation::Interpolation(interpolation) => interpolation, VariableDeclOrInterpolation::VariableDecl(decl) => { return Ok(AstStmt::VariableDecl(decl)) } } } else { self.parse_interpolated_identifier()? }; self.whitespace()?; self.expect_char(':')?; if parse_custom_properties && name.initial_plain().starts_with("--") { let interpolation = self.parse_interpolated_declaration_value(false, false, true)?; let value_span = self.toks_mut().span_from(start); let value = AstExpr::String(StringExpr(interpolation, QuoteKind::None), value_span) .span(value_span); self.expect_statement_separator(Some("custom property"))?; return Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: Vec::new(), span: value_span, })); } self.whitespace()?; if self.looking_at_children()? { if self.is_plain_css() { return Err(( "Nested declarations aren't allowed in plain CSS.", self.toks().current_span(), ) .into()); } if name.initial_plain().starts_with("--") { return Err(( "Declarations whose names begin with \"--\" may not be nested", self.toks_mut().span_from(start), ) .into()); } let children = self.with_children(Self::parse_declaration_child)?.node; return Ok(AstStmt::Style(AstStyle { name, value: None, body: children, span: self.toks_mut().span_from(start), })); } let value = self.parse_expression(None, None, None)?; if self.looking_at_children()? { if self.is_plain_css() { return Err(( "Nested declarations aren't allowed in plain CSS.", self.toks().current_span(), ) .into()); } if name.initial_plain().starts_with("--") && !matches!(value.node, AstExpr::String(..)) { return Err(( "Declarations whose names begin with \"--\" may not be nested", self.toks_mut().span_from(start), ) .into()); } let children = self.with_children(Self::parse_declaration_child)?.node; Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: children, span: self.toks_mut().span_from(start), })) } else { self.expect_statement_separator(None)?; Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: Vec::new(), span: self.toks_mut().span_from(start), })) } } fn parse_single_interpolation(&mut self) -> SassResult { self.expect_char('#')?; self.expect_char('{')?; self.whitespace()?; let contents = self.parse_expression(None, None, None)?; self.expect_char('}')?; if self.is_plain_css() { return Err(("Interpolation isn't allowed in plain CSS.", contents.span).into()); } let mut interpolation = Interpolation::new(); interpolation .contents .push(InterpolationPart::Expr(contents)); Ok(interpolation) } fn parse_interpolated_identifier_body(&mut self, buffer: &mut Interpolation) -> SassResult<()> { while let Some(next) = self.toks().peek() { match next.kind { 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => { buffer.add_char(next.kind); self.toks_mut().next(); } '\\' => { buffer.add_string(self.parse_escape(false)?); } '#' if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { buffer.add_interpolation(self.parse_single_interpolation()?); } _ => break, } } Ok(()) } fn parse_interpolated_identifier(&mut self) -> SassResult { let mut buffer = Interpolation::new(); if self.scan_char('-') { buffer.add_char('-'); if self.scan_char('-') { buffer.add_char('-'); self.parse_interpolated_identifier_body(&mut buffer)?; return Ok(buffer); } } match self.toks().peek() { Some(tok) if is_name_start(tok.kind) => { buffer.add_char(tok.kind); self.toks_mut().next(); } Some(Token { kind: '\\', .. }) => { buffer.add_string(self.parse_escape(true)?); } Some(Token { kind: '#', .. }) if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { buffer.add_interpolation(self.parse_single_interpolation()?); } Some(..) | None => { return Err(("Expected identifier.", self.toks().current_span()).into()) } } self.parse_interpolated_identifier_body(&mut buffer)?; Ok(buffer) } fn looking_at_interpolated_identifier(&mut self) -> bool { let first = match self.toks().peek() { Some(Token { kind: '\\', .. }) => return true, Some(Token { kind: '#', .. }) => { return matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) } Some(Token { kind, .. }) if is_name_start(kind) => return true, Some(tok) => tok, None => return false, }; if first.kind != '-' { return false; } match self.toks().peek_n(1) { Some(Token { kind: '#', .. }) => { matches!(self.toks().peek_n(2), Some(Token { kind: '{', .. })) } Some(Token { kind: '\\' | '-', .. }) => true, Some(Token { kind, .. }) => is_name_start(kind), None => false, } } fn parse_loud_comment(&mut self) -> SassResult { let start = self.toks().cursor(); self.expect_char('/')?; self.expect_char('*')?; let mut buffer = Interpolation::new_plain("/*".to_owned()); while let Some(tok) = self.toks().peek() { match tok.kind { '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } } '*' => { self.toks_mut().next(); buffer.add_char(tok.kind); if self.scan_char('/') { buffer.add_char('/'); return Ok(AstLoudComment { text: buffer, span: self.toks_mut().span_from(start), }); } } '\r' => { self.toks_mut().next(); // todo: does \r even exist at this point? (removed by lexer) if !self.toks_mut().next_char_is('\n') { buffer.add_char('\n'); } } _ => { buffer.add_char(tok.kind); self.toks_mut().next(); } } } Err(("expected more input.", self.toks().current_span()).into()) } fn parse_interpolated_declaration_value( &mut self, // default=false allow_semicolon: bool, // default=false allow_empty: bool, // default=true allow_colon: bool, ) -> SassResult { let mut buffer = Interpolation::new(); let mut brackets = Vec::new(); let mut wrote_newline = false; while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { buffer.add_string(self.parse_escape(true)?); wrote_newline = false; } '"' | '\'' => { buffer.add_interpolation( self.parse_interpolated_string()? .node .as_interpolation(false), ); wrote_newline = false; } '/' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) { let comment = self.fallible_raw_text(Self::skip_loud_comment)?; buffer.add_string(comment); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } wrote_newline = false; } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { // Add a full interpolated identifier to handle cases like // "#{...}--1", since "--1" isn't a valid identifier on its own. buffer.add_interpolation(self.parse_interpolated_identifier()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } wrote_newline = false; } ' ' | '\t' => { if wrote_newline || !matches!( self.toks().peek_n(1), Some(Token { kind: ' ' | '\r' | '\t' | '\n', .. }) ) { self.toks_mut().next(); buffer.add_char(tok.kind); } else { self.toks_mut().next(); } } '\n' | '\r' => { if self.is_indented() { break; } if !matches!( self.toks().peek_n_backwards(1), Some(Token { kind: '\r' | '\n', .. }) ) { buffer.add_char('\n'); } self.toks_mut().next(); wrote_newline = true; } '(' | '{' | '[' => { self.toks_mut().next(); buffer.add_char(tok.kind); brackets.push(opposite_bracket(tok.kind)); wrote_newline = false; } ')' | '}' | ']' => { if brackets.is_empty() { break; } buffer.add_char(tok.kind); self.expect_char(brackets.pop().unwrap())?; wrote_newline = false; } ';' => { if !allow_semicolon && brackets.is_empty() { break; } buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; } ':' => { if !allow_colon && brackets.is_empty() { break; } buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; } 'u' | 'U' => { let before_url = self.toks().cursor(); if !self.scan_identifier("url", false)? { buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; continue; } match self.try_url_contents(None)? { Some(contents) => { buffer.add_interpolation(contents); } None => { self.toks_mut().set_cursor(before_url); buffer.add_char(tok.kind); self.toks_mut().next(); } } wrote_newline = false; } _ => { if self.looking_at_identifier() { buffer.add_string(self.parse_identifier(false, false)?); } else { buffer.add_char(tok.kind); self.toks_mut().next(); } wrote_newline = false; } } } if let Some(&last) = brackets.last() { self.expect_char(last)?; } if !allow_empty && buffer.contents.is_empty() { return Err(("Expected token.", self.toks().current_span()).into()); } Ok(buffer) } fn parse_expression_until_comma( &mut self, // default=false single_equals: bool, ) -> SassResult> { ValueParser::parse_expression( self, Some(&|parser| { Ok(matches!( parser.toks().peek(), Some(Token { kind: ',', .. }) )) }), false, single_equals, ) } fn parse_argument_invocation( &mut self, for_mixin: bool, allow_empty_second_arg: bool, ) -> SassResult { let start = self.toks().cursor(); self.expect_char('(')?; self.whitespace()?; let mut positional = Vec::new(); let mut named = BTreeMap::new(); let mut rest: Option = None; let mut keyword_rest: Option = None; while self.looking_at_expression() { let expression = self.parse_expression_until_comma(!for_mixin)?; self.whitespace()?; if expression.node.is_variable() && self.scan_char(':') { let name = match expression.node { AstExpr::Variable { name, .. } => name, _ => unreachable!(), }; self.whitespace()?; if named.contains_key(&name.node) { return Err(("Duplicate argument.", name.span).into()); } named.insert( name.node, self.parse_expression_until_comma(!for_mixin)?.node, ); } else if self.scan_char('.') { self.expect_char('.')?; self.expect_char('.')?; if rest.is_none() { rest = Some(expression.node); } else { keyword_rest = Some(expression.node); self.whitespace()?; break; } } else if !named.is_empty() { return Err(( "Positional arguments must come before keyword arguments.", expression.span, ) .into()); } else { positional.push(expression.node); } self.whitespace()?; if !self.scan_char(',') { break; } self.whitespace()?; if allow_empty_second_arg && positional.len() == 1 && named.is_empty() && rest.is_none() && matches!(self.toks().peek(), Some(Token { kind: ')', .. })) { positional.push(AstExpr::String( StringExpr(Interpolation::new(), QuoteKind::None), self.toks().current_span(), )); break; } } self.expect_char(')')?; Ok(ArgumentInvocation { positional, named, rest, keyword_rest, span: self.toks_mut().span_from(start), }) } fn parse_expression( &mut self, parse_until: Option>, inside_bracketed_list: Option, single_equals: Option, ) -> SassResult> { ValueParser::parse_expression( self, parse_until, inside_bracketed_list.unwrap_or(false), single_equals.unwrap_or(false), ) } fn parse_declaration_or_buffer(&mut self) -> SassResult { let start = self.toks().cursor(); let mut name_buffer = Interpolation::new(); // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" // hacks. let first = self.toks().peek(); let mut starts_with_punctuation = false; if matches!( first, Some(Token { kind: ':' | '*' | '.', .. }) ) || (matches!(first, Some(Token { kind: '#', .. })) && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) { starts_with_punctuation = true; name_buffer.add_char(self.toks_mut().next().unwrap().kind); name_buffer.add_string(self.raw_text(Self::whitespace)); } if !self.looking_at_interpolated_identifier() { return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } let variable_or_interpolation = if starts_with_punctuation { VariableDeclOrInterpolation::Interpolation(self.parse_interpolated_identifier()?) } else { self.parse_variable_declaration_or_interpolation()? }; match variable_or_interpolation { VariableDeclOrInterpolation::Interpolation(int) => name_buffer.add_interpolation(int), VariableDeclOrInterpolation::VariableDecl(v) => { return Ok(DeclarationOrBuffer::Stmt(AstStmt::VariableDecl(v))) } } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); if self.next_matches("/*") { name_buffer.add_string(self.fallible_raw_text(Self::skip_loud_comment)?); } let mut mid_buffer = String::new(); mid_buffer.push_str(&self.raw_text(Self::whitespace)); if !self.scan_char(':') { if !mid_buffer.is_empty() { name_buffer.add_char(' '); } return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } mid_buffer.push(':'); // Parse custom properties as declarations no matter what. if name_buffer.initial_plain().starts_with("--") { let value_start = self.toks().cursor(); let value = self.parse_interpolated_declaration_value(false, false, true)?; let value_span = self.toks_mut().span_from(value_start); self.expect_statement_separator(Some("custom property"))?; return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some( AstExpr::String(StringExpr(value, QuoteKind::None), value_span) .span(value_span), ), span: self.toks_mut().span_from(start), body: Vec::new(), }))); } if self.scan_char(':') { name_buffer.add_string(mid_buffer); name_buffer.add_char(':'); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } else if self.is_indented() && self.looking_at_interpolated_identifier() { // In the indented syntax, `foo:bar` is always considered a selector // rather than a property. name_buffer.add_string(mid_buffer); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } let post_colon_whitespace = self.raw_text(Self::whitespace); if self.looking_at_children()? { let body = self.with_children(Self::parse_declaration_child)?.node; return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: None, span: self.toks_mut().span_from(start), body, }))); } mid_buffer.push_str(&post_colon_whitespace); let could_be_selector = post_colon_whitespace.is_empty() && self.looking_at_interpolated_identifier(); let before_decl = self.toks().cursor(); let mut calculate_value = || { let value = self.parse_expression(None, None, None)?; if self.looking_at_children()? { if could_be_selector { self.expect_statement_separator(None)?; } } else if !self.at_end_of_statement() { self.expect_statement_separator(None)?; } Ok(value) }; let value = match calculate_value() { Ok(v) => v, Err(e) => { if !could_be_selector { return Err(e); } self.toks_mut().set_cursor(before_decl); let additional = self.almost_any_value(false)?; if !self.is_indented() && self.toks_mut().next_char_is(';') { return Err(e); } name_buffer.add_string(mid_buffer); name_buffer.add_interpolation(additional); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } }; if self.looking_at_children()? { let body = self.with_children(Self::parse_declaration_child)?.node; Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some(value), span: self.toks_mut().span_from(start), body, }))) } else { self.expect_statement_separator(None)?; Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some(value), span: self.toks_mut().span_from(start), body: Vec::new(), }))) } } fn parse_declaration_child(&mut self) -> SassResult { let start = self.toks().cursor(); if self.toks_mut().next_char_is('@') { self.parse_declaration_at_rule(start) } else { self.parse_property_or_variable_declaration(false) } } fn parse_plain_at_rule_name(&mut self) -> SassResult { self.expect_char('@')?; let name = self.parse_identifier(false, false)?; self.whitespace()?; Ok(name) } fn parse_declaration_at_rule(&mut self, start: usize) -> SassResult { let name = self.parse_plain_at_rule_name()?; match name.as_str() { "content" => self.parse_content_rule(start), "debug" => self.parse_debug_rule(), "each" => self.parse_each_rule(Self::parse_declaration_child), "else" => self.parse_disallowed_at_rule(start), "error" => self.parse_error_rule(), "for" => self.parse_for_rule(Self::parse_declaration_child), "if" => self.parse_if_rule(Self::parse_declaration_child), "include" => self.parse_include_rule(), "warn" => self.parse_warn_rule(), "while" => self.parse_while_rule(Self::parse_declaration_child), _ => self.parse_disallowed_at_rule(start), } } fn parse_variable_declaration_or_style_rule(&mut self) -> SassResult { let start = self.toks().cursor(); if self.is_plain_css() { return self.parse_style_rule(None, None); } // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. if self.is_indented() && self.scan_char('\\') { return self.parse_style_rule(None, None); }; if !self.looking_at_identifier() { return self.parse_style_rule(None, None); } match self.parse_variable_declaration_or_interpolation()? { VariableDeclOrInterpolation::VariableDecl(var) => Ok(AstStmt::VariableDecl(var)), VariableDeclOrInterpolation::Interpolation(int) => { self.parse_style_rule(Some(int), Some(start)) } } } fn parse_style_rule( &mut self, existing_buffer: Option, start: Option, ) -> SassResult { let start = start.unwrap_or_else(|| self.toks().cursor()); self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); let mut interpolation = self.parse_style_rule_selector()?; if let Some(mut existing_buffer) = existing_buffer { existing_buffer.add_interpolation(interpolation); interpolation = existing_buffer; } if interpolation.contents.is_empty() { return Err(("expected \"}\".", self.toks().current_span()).into()); } let was_in_style_rule = self.flags().in_style_rule(); *self.flags_mut() |= ContextFlags::IN_STYLE_RULE; let selector_span = self.toks_mut().span_from(start); let children = self.with_children(Self::parse_statement)?; self.flags_mut() .set(ContextFlags::IN_STYLE_RULE, was_in_style_rule); let span = selector_span.merge(children.span); Ok(AstStmt::RuleSet(AstRuleSet { selector: interpolation, body: children.node, selector_span, span, })) } fn parse_silent_comment(&mut self) -> SassResult { let start = self.toks().cursor(); debug_assert!(self.next_matches("//")); self.toks_mut().next(); self.toks_mut().next(); let mut buffer = String::new(); while let Some(tok) = self.toks_mut().next() { if tok.kind == '\n' { self.whitespace_without_comments(); if self.next_matches("//") { self.toks_mut().next(); self.toks_mut().next(); buffer.clear(); continue; } break; } buffer.push(tok.kind); } if self.is_plain_css() { return Err(( "Silent comments aren't allowed in plain CSS.", self.toks_mut().span_from(start), ) .into()); } self.whitespace_without_comments(); Ok(AstStmt::SilentComment(AstSilentComment { text: buffer, span: self.toks_mut().span_from(start), })) } fn next_is_hex(&self) -> bool { match self.toks().peek() { Some(Token { kind, .. }) => kind.is_ascii_hexdigit(), None => false, } } fn assert_public(ident: &str, span: Span) -> SassResult<()> { if !ScssParser::is_private(ident) { return Ok(()); } Err(( "Private members can't be accessed from outside their modules.", span, ) .into()) } fn is_private(ident: &str) -> bool { ident.starts_with('-') || ident.starts_with('_') } fn parse_variable_declaration_without_namespace( &mut self, namespace: Option>, start: Option, ) -> SassResult { let start = start.unwrap_or_else(|| self.toks().cursor()); let name = self.parse_variable_name()?; if namespace.is_some() { Self::assert_public(&name, self.toks_mut().span_from(start))?; } if self.is_plain_css() { return Err(( "Sass variables aren't allowed in plain CSS.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let value = self.parse_expression(None, None, None)?.node; let mut is_guarded = false; let mut is_global = false; while self.scan_char('!') { let flag_start = self.toks().cursor(); let flag = self.parse_identifier(false, false)?; match flag.as_str() { "default" => is_guarded = true, "global" => { if namespace.is_some() { return Err(( "!global isn't allowed for variables in other modules.", self.toks_mut().span_from(flag_start), ) .into()); } is_global = true; } _ => { return Err( ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), ) } } self.whitespace()?; } self.expect_statement_separator(Some("variable declaration"))?; let declaration = AstVariableDecl { namespace, name: Identifier::from(name), value, is_guarded, is_global, span: self.toks_mut().span_from(start), }; if is_global { // todo // _globalVariables.putIfAbsent(name, () => declaration) } Ok(declaration) } fn almost_any_value( &mut self, // default=false omit_comments: bool, ) -> SassResult { let mut buffer = Interpolation::new(); while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { // Write a literal backslash because this text will be re-parsed. buffer.add_char(tok.kind); self.toks_mut().next(); match self.toks_mut().next() { Some(tok) => buffer.add_char(tok.kind), None => { return Err(("expected more input.", self.toks().current_span()).into()) } } } '"' | '\'' => { buffer.add_interpolation( self.parse_interpolated_string()? .node .as_interpolation(false), ); } '/' => { let comment_start = self.toks().cursor(); if self.scan_comment()? { if !omit_comments { buffer.add_string(self.toks().raw_text(comment_start)); } } else { buffer.add_char(self.toks_mut().next().unwrap().kind); } } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { // Add a full interpolated identifier to handle cases like // "#{...}--1", since "--1" isn't a valid identifier on its own. buffer.add_interpolation(self.parse_interpolated_identifier()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } } '\r' | '\n' => { if self.is_indented() { break; } buffer.add_char(self.toks_mut().next().unwrap().kind); } '!' | ';' | '{' | '}' => break, 'u' | 'U' => { let before_url = self.toks().cursor(); if !self.scan_identifier("url", false)? { self.toks_mut().next(); buffer.add_char(tok.kind); continue; } match self.try_url_contents(None)? { Some(contents) => buffer.add_interpolation(contents), None => { self.toks_mut().set_cursor(before_url); self.toks_mut().next(); buffer.add_char(tok.kind); } } } _ => { if self.looking_at_identifier() { buffer.add_string(self.parse_identifier(false, false)?); } else { buffer.add_char(self.toks_mut().next().unwrap().kind); } } } } Ok(buffer) } fn parse_variable_declaration_or_interpolation( &mut self, ) -> SassResult { if !self.looking_at_identifier() { return Ok(VariableDeclOrInterpolation::Interpolation( self.parse_interpolated_identifier()?, )); } let start = self.toks().cursor(); let ident = self.parse_identifier(false, false)?; if self.next_matches(".$") { let namespace_span = self.toks_mut().span_from(start); self.expect_char('.')?; Ok(VariableDeclOrInterpolation::VariableDecl( self.parse_variable_declaration_without_namespace( Some(Spanned { node: Identifier::from(ident), span: namespace_span, }), Some(start), )?, )) } else { let mut buffer = Interpolation::new_plain(ident); if self.looking_at_interpolated_identifier_body() { buffer.add_interpolation(self.parse_interpolated_identifier()?); } Ok(VariableDeclOrInterpolation::Interpolation(buffer)) } } fn looking_at_interpolated_identifier_body(&mut self) -> bool { match self.toks().peek() { Some(Token { kind: '\\', .. }) => true, Some(Token { kind: '#', .. }) if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { true } Some(Token { kind, .. }) if is_name(kind) => true, Some(..) | None => false, } } fn expression_until_comparison(&mut self) -> SassResult> { let value = self.parse_expression( Some(&|parser| { Ok(match parser.toks().peek() { Some(Token { kind: '>', .. }) | Some(Token { kind: '<', .. }) => true, Some(Token { kind: '=', .. }) => { !matches!(parser.toks().peek_n(1), Some(Token { kind: '=', .. })) } _ => false, }) }), None, None, )?; Ok(value) } fn parse_media_query_list(&mut self) -> SassResult { let mut buf = Interpolation::new(); loop { self.whitespace()?; self.parse_media_query(&mut buf)?; self.whitespace()?; if !self.scan_char(',') { break; } buf.add_char(','); buf.add_char(' '); } Ok(buf) } fn parse_media_in_parens(&mut self, buf: &mut Interpolation) -> SassResult<()> { self.expect_char_with_message('(', "media condition in parentheses")?; buf.add_char('('); self.whitespace()?; if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { self.parse_media_in_parens(buf)?; self.whitespace()?; if self.scan_identifier("and", false)? { buf.add_string(" and ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "and")?; } else if self.scan_identifier("or", false)? { buf.add_string(" or ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "or")?; } } else if self.scan_identifier("not", false)? { buf.add_string("not ".to_owned()); self.expect_whitespace()?; self.parse_media_or_interpolation(buf)?; } else { buf.add_expr(self.expression_until_comparison()?); if self.scan_char(':') { self.whitespace()?; buf.add_char(':'); buf.add_char(' '); buf.add_expr(self.parse_expression(None, None, None)?); } else { let next = self.toks().peek(); if matches!( next, Some(Token { kind: '<' | '>' | '=', .. }) ) { let next = next.unwrap().kind; buf.add_char(' '); buf.add_char(self.toks_mut().next().unwrap().kind); if (next == '<' || next == '>') && self.scan_char('=') { buf.add_char('='); } buf.add_char(' '); self.whitespace()?; buf.add_expr(self.expression_until_comparison()?); if (next == '<' || next == '>') && self.scan_char(next) { buf.add_char(' '); buf.add_char(next); if self.scan_char('=') { buf.add_char('='); } buf.add_char(' '); self.whitespace()?; buf.add_expr(self.expression_until_comparison()?); } } } } self.expect_char(')')?; self.whitespace()?; buf.add_char(')'); Ok(()) } fn parse_media_logic_sequence( &mut self, buf: &mut Interpolation, operator: &'static str, ) -> SassResult<()> { loop { self.parse_media_or_interpolation(buf)?; self.whitespace()?; if !self.scan_identifier(operator, false)? { return Ok(()); } self.expect_whitespace()?; buf.add_char(' '); buf.add_string(operator.to_owned()); buf.add_char(' '); } } fn parse_media_or_interpolation(&mut self, buf: &mut Interpolation) -> SassResult<()> { if self.toks_mut().next_char_is('#') { buf.add_interpolation(self.parse_single_interpolation()?); } else { self.parse_media_in_parens(buf)?; } Ok(()) } fn parse_media_query(&mut self, buf: &mut Interpolation) -> SassResult<()> { if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { self.parse_media_in_parens(buf)?; self.whitespace()?; if self.scan_identifier("and", false)? { buf.add_string(" and ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "and")?; } else if self.scan_identifier("or", false)? { buf.add_string(" or ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "or")?; } return Ok(()); } let ident1 = self.parse_interpolated_identifier()?; if ident1.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { // For example, "@media not (...) {" self.expect_whitespace()?; if !self.looking_at_interpolated_identifier() { buf.add_string("not ".to_owned()); self.parse_media_or_interpolation(buf)?; return Ok(()); } } self.whitespace()?; buf.add_interpolation(ident1); if !self.looking_at_interpolated_identifier() { // For example, "@media screen {". return Ok(()); } buf.add_char(' '); let ident2 = self.parse_interpolated_identifier()?; if ident2.as_plain().unwrap_or("").to_ascii_lowercase() == "and" { self.expect_whitespace()?; // For example, "@media screen and ..." buf.add_string(" and ".to_owned()); } else { self.whitespace()?; buf.add_interpolation(ident2); if self.scan_identifier("and", false)? { // For example, "@media only screen and ..." self.expect_whitespace()?; buf.add_string(" and ".to_owned()); } else { // For example, "@media only screen {" return Ok(()); } } // We've consumed either `IDENTIFIER "and"` or // `IDENTIFIER IDENTIFIER "and"`. if self.scan_identifier("not", false)? { // For example, "@media screen and not (...) {" self.expect_whitespace()?; buf.add_string("not ".to_owned()); self.parse_media_or_interpolation(buf)?; return Ok(()); } self.parse_media_logic_sequence(buf, "and")?; Ok(()) } } grass-0.13.4/crates/compiler/src/parse/value.rs000066400000000000000000001724711465374720000214200ustar00rootroot00000000000000use std::{iter::Iterator, marker::PhantomData, sync::Arc}; use codemap::Spanned; use crate::{ ast::*, color::{Color, ColorFormat, NAMED_COLORS}, common::{unvendor, BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp}, error::SassResult, unit::Unit, utils::{as_hex, opposite_bracket}, value::{CalculationName, Number}, ContextFlags, Token, }; use super::StylesheetParser; pub(crate) type Predicate<'c, P> = &'c dyn Fn(&mut P) -> SassResult; fn is_hex_color(interpolation: &Interpolation) -> bool { if let Some(plain) = interpolation.as_plain() { if ![3, 4, 6, 8].contains(&plain.len()) { return false; } return plain.chars().all(|c| c.is_ascii_hexdigit()); } false } pub(crate) struct ValueParser<'a, 'c, P: StylesheetParser<'a>> { comma_expressions: Option>>, space_expressions: Option>>, binary_operators: Option>, operands: Option>>, allow_slash: bool, single_expression: Option>, start: usize, inside_bracketed_list: bool, single_equals: bool, parse_until: Option>, _a: PhantomData<&'a ()>, } impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { pub fn parse_expression( parser: &mut P, parse_until: Option>, inside_bracketed_list: bool, single_equals: bool, ) -> SassResult> { let start = parser.toks().cursor(); let mut value_parser = Self::new(parser, parse_until, inside_bracketed_list, single_equals); if let Some(parse_until) = value_parser.parse_until { if parse_until(parser)? { return Err(("Expected expression.", parser.toks().current_span()).into()); } } if value_parser.inside_bracketed_list { let bracket_start = parser.toks().cursor(); parser.expect_char('[')?; parser.whitespace()?; if parser.scan_char(']') { return Ok(AstExpr::List(ListExpr { elems: Vec::new(), separator: ListSeparator::Undecided, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(bracket_start))); } }; value_parser.start = parser.toks().cursor(); value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?); let mut value = value_parser.parse_value(parser)?; value.span = parser.toks_mut().span_from(start); Ok(value) } pub fn new( parser: &mut P, parse_until: Option>, inside_bracketed_list: bool, single_equals: bool, ) -> Self { Self { comma_expressions: None, space_expressions: None, binary_operators: None, operands: None, allow_slash: true, start: parser.toks().cursor(), single_expression: None, parse_until, inside_bracketed_list, single_equals, _a: PhantomData, } } /// Parse a value from a stream of tokens /// /// This function will cease parsing if the predicate returns true. pub(crate) fn parse_value(&mut self, parser: &mut P) -> SassResult> { parser.whitespace()?; let start = parser.toks().cursor(); let was_in_parens = parser.flags().in_parens(); loop { parser.whitespace()?; if let Some(parse_until) = self.parse_until { if parse_until(parser)? { break; } } let first = parser.toks().peek(); match first { Some(Token { kind: '(', .. }) => { let expr = self.parse_paren_expr(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '[', .. }) => { let expr = parser.parse_expression(None, Some(true), None)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '$', .. }) => { let expr = Self::parse_variable(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '&', .. }) => { let expr = Self::parse_selector(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => { let expr = parser .parse_interpolated_string()? .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start))); self.add_single_expression(expr, parser)?; } Some(Token { kind: '#', .. }) => { let expr = self.parse_hash(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '=', .. }) => { parser.toks_mut().next(); if self.single_equals && !matches!(parser.toks().peek(), Some(Token { kind: '=', .. })) { self.add_operator( Spanned { node: BinaryOp::SingleEq, span: parser.toks_mut().span_from(start), }, parser, )?; } else { parser.expect_char('=')?; self.add_operator( Spanned { node: BinaryOp::Equal, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '!', .. }) => match parser.toks().peek_n(1) { Some(Token { kind: '=', .. }) => { parser.toks_mut().next(); parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::NotEqual, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind, .. }) if kind.is_ascii_whitespace() || kind == 'i' || kind == 'I' => { let expr = Self::parse_important_expr(parser)?; self.add_single_expression(expr, parser)?; } None => { let expr = Self::parse_important_expr(parser)?; self.add_single_expression(expr, parser)?; } Some(..) => break, }, Some(Token { kind: '<', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: if parser.scan_char('=') { BinaryOp::LessThanEqual } else { BinaryOp::LessThan }, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind: '>', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: if parser.scan_char('=') { BinaryOp::GreaterThanEqual } else { BinaryOp::GreaterThan }, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind: '*', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Mul, span: parser.toks().current_span(), }, parser, )?; } Some(Token { kind: '+', .. }) => { if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Plus, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '-', .. }) => { if matches!( parser.toks().peek_n(1), Some(Token { kind: '0'..='9' | '.', .. }) ) && (self.single_expression.is_none() || matches!( parser.toks_mut().peek_previous(), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) )) { let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } else if parser.looking_at_interpolated_identifier() { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } else if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Minus, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '/', .. }) => { if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Div, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '%', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Rem, span: parser.toks().current_span(), }, parser, )?; } Some(Token { kind: '0'..='9', .. }) => { let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '.', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { break; } let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: 'a', .. }) => { if !parser.is_plain_css() && parser.scan_identifier("and", false)? { self.add_operator( Spanned { node: BinaryOp::And, span: parser.toks_mut().span_from(start), }, parser, )?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'o', .. }) => { if !parser.is_plain_css() && parser.scan_identifier("or", false)? { self.add_operator( Spanned { node: BinaryOp::Or, span: parser.toks_mut().span_from(start), }, parser, )?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { let expr = Self::parse_unicode_range(parser)?; self.add_single_expression(expr, parser)?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'b'..='z', .. }) | Some(Token { kind: 'A'..='Z', .. }) | Some(Token { kind: '_', .. }) | Some(Token { kind: '\\', .. }) | Some(Token { kind: '\u{80}'..=std::char::MAX, .. }) => { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: ',', .. }) => { // If we discover we're parsing a list whose first element is a // division operation, and we're in parentheses, reparse outside of a // paren context. This ensures that `(1/2, 1)` doesn't perform division // on its first element. if parser.flags().in_parens() { parser.flags_mut().set(ContextFlags::IN_PARENS, false); if self.allow_slash { self.reset_state(parser)?; continue; } // todo: does this branch ever get hit } if self.single_expression.is_none() { return Err(("Expected expression.", parser.toks().current_span()).into()); } self.resolve_space_expressions(parser)?; // [resolveSpaceExpressions] can modify [singleExpression_], but it // can't set it to null`. self.comma_expressions .get_or_insert_with(Default::default) .push(self.single_expression.take().unwrap()); parser.toks_mut().next(); self.allow_slash = true; } Some(..) | None => break, } } if self.inside_bracketed_list { parser.expect_char(']')?; } if self.comma_expressions.is_some() { self.resolve_space_expressions(parser)?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parens); if let Some(single_expression) = self.single_expression.take() { self.comma_expressions .as_mut() .unwrap() .push(single_expression); } Ok(AstExpr::List(ListExpr { elems: self.comma_expressions.take().unwrap(), separator: ListSeparator::Comma, brackets: if self.inside_bracketed_list { Brackets::Bracketed } else { Brackets::None }, }) .span(parser.toks_mut().span_from(start))) } else if self.inside_bracketed_list && self.space_expressions.is_some() { self.resolve_operations(parser)?; self.space_expressions .as_mut() .unwrap() .push(self.single_expression.take().unwrap()); Ok(AstExpr::List(ListExpr { elems: self.space_expressions.take().unwrap(), separator: ListSeparator::Space, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(start))) } else { self.resolve_space_expressions(parser)?; if self.inside_bracketed_list { return Ok(AstExpr::List(ListExpr { elems: vec![self.single_expression.take().unwrap()], separator: ListSeparator::Undecided, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(start))); } Ok(self.single_expression.take().unwrap()) } } fn parse_single_expression(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); let first = parser.toks().peek(); match first { Some(Token { kind: '(', .. }) => self.parse_paren_expr(parser), Some(Token { kind: '/', .. }) => self.parse_unary_operation(parser), Some(Token { kind: '[', .. }) => Self::parse_expression(parser, None, true, false), Some(Token { kind: '$', .. }) => Self::parse_variable(parser), Some(Token { kind: '&', .. }) => Self::parse_selector(parser), Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => Ok(parser .parse_interpolated_string()? .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start)))), Some(Token { kind: '#', .. }) => self.parse_hash(parser), Some(Token { kind: '+', .. }) => self.parse_plus_expr(parser), Some(Token { kind: '-', .. }) => self.parse_minus_expr(parser), Some(Token { kind: '!', .. }) => Self::parse_important_expr(parser), Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { Self::parse_unicode_range(parser) } else { self.parse_identifier_like(parser) } } Some(Token { kind: '0'..='9', .. }) | Some(Token { kind: '.', .. }) => ValueParser::parse_number(parser), Some(Token { kind: 'a'..='z', .. }) | Some(Token { kind: 'A'..='Z', .. }) | Some(Token { kind: '_', .. }) | Some(Token { kind: '\\', .. }) | Some(Token { kind: '\u{80}'..=std::char::MAX, .. }) => self.parse_identifier_like(parser), Some(..) | None => Err(( "Expected expression.", parser.toks_mut().span_from(self.start), ) .into()), } } fn resolve_one_operation(&mut self, parser: &mut P) -> SassResult<()> { let operator = self.binary_operators.as_mut().unwrap().pop().unwrap(); let operands = self.operands.as_mut().unwrap(); let left = operands.pop().unwrap(); let right = match self.single_expression.take() { Some(val) => val, None => return Err(("Expected expression.", left.span).into()), }; let span = left.span.merge(right.span); if self.allow_slash && !parser.flags().in_parens() && operator == BinaryOp::Div && left.node.is_slash_operand() && right.node.is_slash_operand() { self.single_expression = Some(AstExpr::slash(left.node, right.node, span).span(span)); } else { self.single_expression = Some( AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: left.node, op: operator, rhs: right.node, allows_slash: false, span, })) .span(span), ); self.allow_slash = false; } Ok(()) } fn resolve_operations(&mut self, parser: &mut P) -> SassResult<()> { loop { let should_break = match self.binary_operators.as_ref() { Some(bin) => bin.is_empty(), None => true, }; if should_break { break; } self.resolve_one_operation(parser)?; } Ok(()) } fn add_single_expression( &mut self, expression: Spanned, parser: &mut P, ) -> SassResult<()> { if self.single_expression.is_some() { // If we discover we're parsing a list whose first element is a division // operation, and we're in parentheses, reparse outside of a paren // context. This ensures that `(1/2 1)` doesn't perform division on its // first element. if parser.flags().in_parens() { parser.flags_mut().set(ContextFlags::IN_PARENS, false); if self.allow_slash { self.reset_state(parser)?; return Ok(()); } } if self.space_expressions.is_none() { self.space_expressions = Some(Vec::new()); } self.resolve_operations(parser)?; self.space_expressions .as_mut() .unwrap() .push(self.single_expression.take().unwrap()); self.allow_slash = true; } self.single_expression = Some(expression); Ok(()) } fn add_operator(&mut self, op: Spanned, parser: &mut P) -> SassResult<()> { if parser.is_plain_css() && op.node != BinaryOp::Div && op.node != BinaryOp::SingleEq { return Err(("Operators aren't allowed in plain CSS.", op.span).into()); } self.allow_slash = self.allow_slash && op.node == BinaryOp::Div; if self.binary_operators.is_none() { self.binary_operators = Some(Vec::new()); } if self.operands.is_none() { self.operands = Some(Vec::new()); } while let Some(last_op) = self.binary_operators.as_ref().unwrap_or(&Vec::new()).last() { if last_op.precedence() < op.precedence() { break; } self.resolve_one_operation(parser)?; } self.binary_operators .get_or_insert_with(Default::default) .push(op.node); match self.single_expression.take() { Some(expr) => { self.operands.get_or_insert_with(Vec::new).push(expr); } None => return Err(("Expected expression.", op.span).into()), } parser.whitespace()?; self.single_expression = Some(self.parse_single_expression(parser)?); Ok(()) } fn resolve_space_expressions(&mut self, parser: &mut P) -> SassResult<()> { self.resolve_operations(parser)?; if let Some(mut space_expressions) = self.space_expressions.take() { let single_expression = match self.single_expression.take() { Some(val) => val, None => return Err(("Expected expression.", parser.toks().current_span()).into()), }; let span = single_expression.span; space_expressions.push(single_expression); self.single_expression = Some( AstExpr::List(ListExpr { elems: space_expressions, separator: ListSeparator::Space, brackets: Brackets::None, }) .span(span), ); } Ok(()) } fn parse_map( parser: &mut P, first: Spanned, start: usize, ) -> SassResult> { let mut pairs = vec![(first, parser.parse_expression_until_comma(false)?.node)]; while parser.scan_char(',') { parser.whitespace()?; if !parser.looking_at_expression() { break; } let key = parser.parse_expression_until_comma(false)?; parser.expect_char(':')?; parser.whitespace()?; let value = parser.parse_expression_until_comma(false)?; pairs.push((key, value.node)); } parser.expect_char(')')?; Ok(AstExpr::Map(AstSassMap(pairs)).span(parser.toks_mut().span_from(start))) } fn parse_paren_expr(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); if parser.is_plain_css() { return Err(( "Parentheses aren't allowed in plain CSS.", parser.toks().current_span(), ) .into()); } let was_in_parentheses = parser.flags().in_parens(); parser.flags_mut().set(ContextFlags::IN_PARENS, true); parser.expect_char('(')?; parser.whitespace()?; if !parser.looking_at_expression() { parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Ok(AstExpr::List(ListExpr { elems: Vec::new(), separator: ListSeparator::Undecided, brackets: Brackets::None, }) .span(parser.toks_mut().span_from(start))); } let first = parser.parse_expression_until_comma(false)?; if parser.scan_char(':') { parser.whitespace()?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Self::parse_map(parser, first, start); } if !parser.scan_char(',') { parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Ok(AstExpr::Paren(Arc::new(first.node)).span(first.span)); } parser.whitespace()?; let mut expressions = vec![first]; loop { if !parser.looking_at_expression() { break; } expressions.push(parser.parse_expression_until_comma(false)?); if !parser.scan_char(',') { break; } parser.whitespace()?; } parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); Ok(AstExpr::List(ListExpr { elems: expressions, separator: ListSeparator::Comma, brackets: Brackets::None, }) .span(parser.toks_mut().span_from(start))) } fn parse_variable(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); let name = parser.parse_variable_name()?; if parser.is_plain_css() { return Err(( "Sass variables aren't allowed in plain CSS.", parser.toks_mut().span_from(start), ) .into()); } Ok(AstExpr::Variable { name: Spanned { node: Identifier::from(name), span: parser.toks_mut().span_from(start), }, namespace: None, } .span(parser.toks_mut().span_from(start))) } fn parse_selector(parser: &mut P) -> SassResult> { if parser.is_plain_css() { return Err(( "The parent selector isn't allowed in plain CSS.", parser.toks().current_span(), ) .into()); } let start = parser.toks().cursor(); parser.expect_char('&')?; if parser.toks().next_char_is('&') { // todo: emit a warning here // warn( // 'In Sass, "&&" means two copies of the parent selector. You ' // 'probably want to use "and" instead.', // scanner.spanFrom(start)); // scanner.position--; } Ok(AstExpr::ParentSelector.span(parser.toks_mut().span_from(start))) } fn parse_hash(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); debug_assert!(matches!( parser.toks().peek(), Some(Token { kind: '#', .. }) )); if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { return self.parse_identifier_like(parser); } parser.expect_char('#')?; if matches!( parser.toks().peek(), Some(Token { kind: '0'..='9', .. }) ) { let color = self.parse_hex_color_contents(parser)?; return Ok(AstExpr::Color(Arc::new(color)).span(parser.toks_mut().span_from(start))); } let after_hash = parser.toks().cursor(); let ident = parser.parse_interpolated_identifier()?; if is_hex_color(&ident) { parser.toks_mut().set_cursor(after_hash); let color = self.parse_hex_color_contents(parser)?; return Ok( AstExpr::Color(Arc::new(color)).span(parser.toks_mut().span_from(after_hash)) ); } let mut buffer = Interpolation::new(); buffer.add_char('#'); buffer.add_interpolation(ident); let span = parser.toks_mut().span_from(start); Ok(AstExpr::String(StringExpr(buffer, QuoteKind::None), span).span(span)) } fn parse_hex_digit(&mut self, parser: &mut P) -> SassResult { match parser.toks().peek() { Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => { parser.toks_mut().next(); Ok(as_hex(kind)) } _ => Err(("Expected hex digit.", parser.toks().current_span()).into()), } } fn parse_hex_color_contents(&mut self, parser: &mut P) -> SassResult { let start = parser.toks().cursor(); let digit1 = self.parse_hex_digit(parser)?; let digit2 = self.parse_hex_digit(parser)?; let digit3 = self.parse_hex_digit(parser)?; let red: u32; let green: u32; let blue: u32; let mut alpha: f64 = 1.0; if parser.next_is_hex() { let digit4 = self.parse_hex_digit(parser)?; if parser.next_is_hex() { red = (digit1 << 4) + digit2; green = (digit3 << 4) + digit4; blue = (self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?; if parser.next_is_hex() { alpha = ((self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?) as f64 / 0xff as f64; } } else { // #abcd red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; blue = (digit3 << 4) + digit3; alpha = ((digit4 << 4) + digit4) as f64 / 0xff as f64; } } else { // #abc red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; blue = (digit3 << 4) + digit3; } Ok(Color::new_rgba( Number::from(red), Number::from(green), Number::from(blue), Number(alpha), // todo: // // Don't emit four- or eight-digit hex colors as hex, since that's not // // yet well-supported in browsers. ColorFormat::Literal(parser.toks_mut().raw_text(start - 1)), )) } fn parse_unary_operation(&mut self, parser: &mut P) -> SassResult> { let op_span = parser.toks().current_span(); let operator = Self::expect_unary_operator(parser)?; if parser.is_plain_css() && operator != UnaryOp::Div { return Err(("Operators aren't allowed in plain CSS.", op_span).into()); } parser.whitespace()?; let operand = self.parse_single_expression(parser)?; let span = op_span.merge(parser.toks().current_span()); Ok(AstExpr::UnaryOp(operator, Arc::new(operand.node), span).span(span)) } fn expect_unary_operator(parser: &mut P) -> SassResult { let span = parser.toks().current_span(); Ok(match parser.toks_mut().next() { Some(Token { kind: '+', .. }) => UnaryOp::Plus, Some(Token { kind: '-', .. }) => UnaryOp::Neg, Some(Token { kind: '/', .. }) => UnaryOp::Div, Some(..) | None => return Err(("Expected unary operator.", span).into()), }) } fn consume_natural_number(parser: &mut P) -> SassResult<()> { if !matches!( parser.toks_mut().next(), Some(Token { kind: '0'..='9', .. }) ) { return Err(("Expected digit.", parser.toks().prev_span()).into()); } while matches!( parser.toks().peek(), Some(Token { kind: '0'..='9', .. }) ) { parser.toks_mut().next(); } Ok(()) } fn parse_number(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); if !parser.scan_char('+') { parser.scan_char('-'); } let after_sign = parser.toks().cursor(); if !parser.toks().next_char_is('.') { ValueParser::consume_natural_number(parser)?; } ValueParser::try_decimal(parser, parser.toks().cursor() != after_sign)?; ValueParser::try_exponent(parser)?; let number: f64 = parser.toks_mut().raw_text(start).parse().unwrap(); let unit = if parser.scan_char('%') { Unit::Percent } else if parser.looking_at_identifier() && (!matches!(parser.toks().peek(), Some(Token { kind: '-', .. })) || !matches!(parser.toks().peek_n(1), Some(Token { kind: '-', .. }))) { Unit::from(parser.parse_identifier(false, true)?) } else { Unit::None }; Ok(AstExpr::Number { n: Number::from(number), unit, } .span(parser.toks_mut().span_from(start))) } fn try_decimal(parser: &mut P, allow_trailing_dot: bool) -> SassResult> { if !matches!(parser.toks().peek(), Some(Token { kind: '.', .. })) { return Ok(None); } match parser.toks().peek_n(1) { Some(Token { kind, .. }) if !kind.is_ascii_digit() => { if allow_trailing_dot { return Ok(None); } return Err(("Expected digit.", parser.toks().current_span()).into()); } Some(..) => {} None => return Err(("Expected digit.", parser.toks().current_span()).into()), } let mut buffer = String::new(); parser.expect_char('.')?; buffer.push('.'); while let Some(Token { kind, .. }) = parser.toks().peek() { if !kind.is_ascii_digit() { break; } buffer.push(kind); parser.toks_mut().next(); } Ok(Some(buffer)) } fn try_exponent(parser: &mut P) -> SassResult> { let mut buffer = String::new(); match parser.toks().peek() { Some(Token { kind: 'e' | 'E', .. }) => buffer.push('e'), _ => return Ok(None), } let next = match parser.toks().peek_n(1) { Some(Token { kind: kind @ ('0'..='9' | '-' | '+'), .. }) => kind, _ => return Ok(None), }; parser.toks_mut().next(); if next == '+' || next == '-' { parser.toks_mut().next(); buffer.push(next); } match parser.toks().peek() { Some(Token { kind: '0'..='9', .. }) => {} _ => return Err(("Expected digit.", parser.toks().current_span()).into()), } while let Some(tok) = parser.toks().peek() { if !tok.kind.is_ascii_digit() { break; } buffer.push(tok.kind); parser.toks_mut().next(); } Ok(Some(buffer)) } fn parse_plus_expr(&mut self, parser: &mut P) -> SassResult> { debug_assert!(parser.toks().next_char_is('+')); match parser.toks().peek_n(1) { Some(Token { kind: '0'..='9' | '.', .. }) => ValueParser::parse_number(parser), _ => self.parse_unary_operation(parser), } } fn parse_minus_expr(&mut self, parser: &mut P) -> SassResult> { debug_assert!(parser.toks().next_char_is('-')); if matches!( parser.toks().peek_n(1), Some(Token { kind: '0'..='9' | '.', .. }) ) { return ValueParser::parse_number(parser); } if parser.looking_at_interpolated_identifier() { return self.parse_identifier_like(parser); } self.parse_unary_operation(parser) } fn parse_important_expr(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); parser.expect_char('!')?; parser.whitespace()?; parser.expect_identifier("important", false)?; let span = parser.toks_mut().span_from(start); Ok(AstExpr::String( StringExpr( Interpolation::new_plain("!important".to_owned()), QuoteKind::None, ), span, ) .span(span)) } fn parse_identifier_like(&mut self, parser: &mut P) -> SassResult> { if let Some(func) = P::IDENTIFIER_LIKE { return func(parser); } let start = parser.toks().cursor(); let identifier = parser.parse_interpolated_identifier()?; let ident_span = parser.toks_mut().span_from(start); let plain = identifier.as_plain(); let lower = plain.map(str::to_ascii_lowercase); if let Some(plain) = plain { if plain == "if" && parser.toks().next_char_is('(') { let call_args = parser.parse_argument_invocation(false, false)?; let span = call_args.span; return Ok(AstExpr::If(Arc::new(Ternary(call_args))).span(span)); } else if plain == "not" { parser.whitespace()?; let value = self.parse_single_expression(parser)?; let span = parser.toks_mut().span_from(start); return Ok(AstExpr::UnaryOp(UnaryOp::Not, Arc::new(value.node), span).span(span)); } let lower_ref = lower.as_ref().unwrap(); if !parser.toks().next_char_is('(') { match plain { "null" => return Ok(AstExpr::Null.span(parser.toks_mut().span_from(start))), "true" => return Ok(AstExpr::True.span(parser.toks_mut().span_from(start))), "false" => return Ok(AstExpr::False.span(parser.toks_mut().span_from(start))), _ => {} } if let Some(color) = NAMED_COLORS.get_by_name(lower_ref.as_str()) { return Ok(AstExpr::Color(Arc::new(Color::new( color[0], color[1], color[2], color[3], plain.to_owned(), ))) .span(parser.toks_mut().span_from(start))); } } if let Some(func) = ValueParser::try_parse_special_function(parser, lower_ref, start)? { return Ok(func); } } match parser.toks().peek() { Some(Token { kind: '.', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { return Ok(AstExpr::String( StringExpr(identifier, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start))); } parser.toks_mut().next(); match plain { Some(s) => Self::namespaced_expression( Spanned { node: Identifier::from(s), span: ident_span, }, start, parser, ), None => Err(("Interpolation isn't allowed in namespaces.", ident_span).into()), } } Some(Token { kind: '(', .. }) => { if let Some(plain) = plain { let arguments = parser.parse_argument_invocation(false, lower.as_deref() == Some("var"))?; Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: None, name: Identifier::from(plain), arguments: Arc::new(arguments), span: parser.toks_mut().span_from(start), }) .span(parser.toks_mut().span_from(start))) } else { let arguments = parser.parse_argument_invocation(false, false)?; Ok( AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: identifier, arguments, span: parser.toks_mut().span_from(start), })) .span(parser.toks_mut().span_from(start)), ) } } _ => Ok(AstExpr::String( StringExpr(identifier, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start))), } } fn namespaced_expression( namespace: Spanned, start: usize, parser: &mut P, ) -> SassResult> { if parser.toks().next_char_is('$') { let name_start = parser.toks().cursor(); let name = parser.parse_variable_name()?; let span = parser.toks_mut().span_from(start); P::assert_public(&name, span)?; if parser.is_plain_css() { return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); } return Ok(AstExpr::Variable { name: Spanned { node: Identifier::from(name), span: parser.toks_mut().span_from(name_start), }, namespace: Some(namespace), } .span(span)); } let name = parser.parse_public_identifier()?; let args = parser.parse_argument_invocation(false, false)?; let span = parser.toks_mut().span_from(start); if parser.is_plain_css() { return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); } Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: Some(namespace), name: Identifier::from(name), arguments: Arc::new(args), span, }) .span(span)) } fn parse_unicode_range(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); parser.expect_ident_char('u', false)?; parser.expect_char('+')?; let mut first_range_length = 0; while let Some(next) = parser.toks().peek() { if !next.kind.is_ascii_hexdigit() { break; } parser.toks_mut().next(); first_range_length += 1; } let mut has_question_mark = false; while parser.scan_char('?') { has_question_mark = true; first_range_length += 1; } let span = parser.toks_mut().span_from(start); if first_range_length == 0 { return Err(("Expected hex digit or \"?\".", parser.toks().current_span()).into()); } else if first_range_length > 6 { return Err(("Expected at most 6 digits.", span).into()); } else if has_question_mark { return Ok(AstExpr::String( StringExpr( Interpolation::new_plain(parser.toks_mut().raw_text(start)), QuoteKind::None, ), span, ) .span(span)); } if parser.scan_char('-') { let second_range_start = parser.toks().cursor(); let mut second_range_length = 0; while let Some(next) = parser.toks().peek() { if !next.kind.is_ascii_hexdigit() { break; } parser.toks_mut().next(); second_range_length += 1; } if second_range_length == 0 { return Err(("Expected hex digit.", parser.toks().current_span()).into()); } else if second_range_length > 6 { return Err(( "Expected at most 6 digits.", parser.toks_mut().span_from(second_range_start), ) .into()); } } if parser.looking_at_interpolated_identifier_body() { return Err(("Expected end of identifier.", parser.toks().current_span()).into()); } let span = parser.toks_mut().span_from(start); Ok(AstExpr::String( StringExpr( Interpolation::new_plain(parser.toks_mut().raw_text(start)), QuoteKind::None, ), span, ) .span(span)) } pub(crate) fn try_parse_special_function( parser: &mut P, name: &str, start: usize, ) -> SassResult>> { if matches!(parser.toks().peek(), Some(Token { kind: '(', .. })) { if let Some(calculation) = ValueParser::try_parse_calculation(parser, name, start)? { return Ok(Some(calculation)); } } let normalized = unvendor(name); let mut buffer; match normalized { "calc" | "element" | "expression" => { if !parser.scan_char('(') { return Ok(None); } buffer = Interpolation::new_plain(name.to_owned()); buffer.add_char('('); } "progid" => { if !parser.scan_char(':') { return Ok(None); } buffer = Interpolation::new_plain(name.to_owned()); buffer.add_char(':'); while let Some(Token { kind, .. }) = parser.toks().peek() { if !kind.is_alphabetic() && kind != '.' { break; } buffer.add_char(kind); parser.toks_mut().next(); } parser.expect_char('(')?; buffer.add_char('('); } "url" => { return Ok(parser.try_url_contents(None)?.map(|contents| { AstExpr::String( StringExpr(contents, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start)) })) } _ => return Ok(None), } buffer.add_interpolation(parser.parse_interpolated_declaration_value(false, true, true)?); parser.expect_char(')')?; buffer.add_char(')'); Ok(Some( AstExpr::String( StringExpr(buffer, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start)), )) } fn contains_calculation_interpolation(parser: &mut P) -> SassResult { let mut parens = 0; let mut brackets = Vec::new(); let start = parser.toks().cursor(); while let Some(next) = parser.toks().peek() { match next.kind { '\\' => { parser.toks_mut().next(); // todo: i wonder if this can be broken (not for us but dart-sass) parser.toks_mut().next(); } '/' => { if !parser.scan_comment()? { parser.toks_mut().next(); } } '\'' | '"' => { parser.parse_interpolated_string()?; } '#' => { if parens == 0 && matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { parser.toks_mut().set_cursor(start); return Ok(true); } parser.toks_mut().next(); } '(' | '{' | '[' => { if next.kind == '(' { parens += 1; } brackets.push(opposite_bracket(next.kind)); parser.toks_mut().next(); } ')' | '}' | ']' => { if next.kind == ')' { parens -= 1; } if brackets.is_empty() || brackets.pop() != Some(next.kind) { parser.toks_mut().set_cursor(start); return Ok(false); } parser.toks_mut().next(); } _ => { parser.toks_mut().next(); } } } parser.toks_mut().set_cursor(start); Ok(false) } fn try_parse_calculation_interpolation( parser: &mut P, start: usize, ) -> SassResult> { Ok( if ValueParser::contains_calculation_interpolation(parser)? { Some(AstExpr::String( StringExpr( parser.parse_interpolated_declaration_value(false, false, true)?, QuoteKind::None, ), parser.toks_mut().span_from(start), )) } else { None }, ) } fn parse_calculation_value(parser: &mut P) -> SassResult> { match parser.toks().peek() { Some(Token { kind: '+' | '-' | '.' | '0'..='9', .. }) => ValueParser::parse_number(parser), Some(Token { kind: '$', .. }) => ValueParser::parse_variable(parser), Some(Token { kind: '(', .. }) => { let start = parser.toks().cursor(); parser.toks_mut().next(); let value = match ValueParser::try_parse_calculation_interpolation(parser, start)? { Some(v) => v, None => { parser.whitespace()?; ValueParser::parse_calculation_sum(parser)?.node } }; parser.whitespace()?; parser.expect_char(')')?; Ok(AstExpr::Paren(Arc::new(value)).span(parser.toks_mut().span_from(start))) } _ if !parser.looking_at_identifier() => Err(( "Expected number, variable, function, or calculation.", parser.toks().current_span(), ) .into()), _ => { let start = parser.toks().cursor(); let ident = parser.parse_identifier(false, false)?; let ident_span = parser.toks_mut().span_from(start); if parser.scan_char('.') { return ValueParser::namespaced_expression( Spanned { node: Identifier::from(&ident), span: ident_span, }, start, parser, ); } if !parser.toks().next_char_is('(') { return Err(("Expected \"(\" or \".\".", parser.toks().current_span()).into()); } let lowercase = ident.to_ascii_lowercase(); let calculation = ValueParser::try_parse_calculation(parser, &lowercase, start)?; if let Some(calc) = calculation { Ok(calc) } else if lowercase == "if" { Ok(AstExpr::If(Arc::new(Ternary( parser.parse_argument_invocation(false, false)?, ))) .span(parser.toks_mut().span_from(start))) } else { Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: None, name: Identifier::from(ident), arguments: Arc::new(parser.parse_argument_invocation(false, false)?), span: parser.toks_mut().span_from(start), }) .span(parser.toks_mut().span_from(start))) } } } } fn parse_calculation_product(parser: &mut P) -> SassResult> { let mut product = ValueParser::parse_calculation_value(parser)?; loop { parser.whitespace()?; match parser.toks().peek() { Some(Token { kind: op @ ('*' | '/'), .. }) => { parser.toks_mut().next(); parser.whitespace()?; let rhs = ValueParser::parse_calculation_value(parser)?; let span = product.span.merge(rhs.span); product.node = AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: product.node, op: if op == '*' { BinaryOp::Mul } else { BinaryOp::Div }, rhs: rhs.node, allows_slash: false, span, })); product.span = span; } _ => return Ok(product), } } } fn parse_calculation_sum(parser: &mut P) -> SassResult> { let mut sum = ValueParser::parse_calculation_product(parser)?; loop { match parser.toks().peek() { Some(Token { kind: next @ ('+' | '-'), .. }) => { if !matches!( parser.toks().peek_n_backwards(1), Some(Token { kind: ' ' | '\t' | '\r' | '\n', .. }) ) || !matches!( parser.toks().peek_n(1), Some(Token { kind: ' ' | '\t' | '\r' | '\n', .. }) ) { return Err(( "\"+\" and \"-\" must be surrounded by whitespace in calculations.", parser.toks().current_span(), ) .into()); } parser.toks_mut().next(); parser.whitespace()?; let rhs = ValueParser::parse_calculation_product(parser)?; let span = sum.span.merge(rhs.span); sum = AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: sum.node, op: if next == '+' { BinaryOp::Plus } else { BinaryOp::Minus }, rhs: rhs.node, allows_slash: false, span, })) .span(span); } _ => return Ok(sum), } } } fn parse_calculation_arguments( parser: &mut P, max_args: Option, start: usize, ) -> SassResult> { parser.expect_char('(')?; if let Some(interpolation) = ValueParser::try_parse_calculation_interpolation(parser, start)? { parser.expect_char(')')?; return Ok(vec![interpolation]); } parser.whitespace()?; let mut arguments = vec![ValueParser::parse_calculation_sum(parser)?.node]; while (max_args.is_none() || arguments.len() < max_args.unwrap()) && parser.scan_char(',') { parser.whitespace()?; arguments.push(ValueParser::parse_calculation_sum(parser)?.node); } parser.expect_char_with_message( ')', if Some(arguments.len()) == max_args { r#""+", "-", "*", "/", or ")""# } else { r#""+", "-", "*", "/", ",", or ")""# }, )?; Ok(arguments) } fn try_parse_calculation( parser: &mut P, name: &str, start: usize, ) -> SassResult>> { debug_assert!(parser.toks().next_char_is('(')); Ok(Some(match name { "calc" => { let args = ValueParser::parse_calculation_arguments(parser, Some(1), start)?; AstExpr::Calculation { name: CalculationName::Calc, args, } .span(parser.toks_mut().span_from(start)) } "min" | "max" => { // min() and max() are parsed as calculations if possible, and otherwise // are parsed as normal Sass functions. let before_args = parser.toks().cursor(); let args = match ValueParser::parse_calculation_arguments(parser, None, start) { Ok(args) => args, Err(..) => { parser.toks_mut().set_cursor(before_args); return Ok(None); } }; AstExpr::Calculation { name: if name == "min" { CalculationName::Min } else { CalculationName::Max }, args, } .span(parser.toks_mut().span_from(start)) } "clamp" => { let args = ValueParser::parse_calculation_arguments(parser, Some(3), start)?; AstExpr::Calculation { name: CalculationName::Clamp, args, } .span(parser.toks_mut().span_from(start)) } _ => return Ok(None), })) } fn reset_state(&mut self, parser: &mut P) -> SassResult<()> { self.comma_expressions = None; self.space_expressions = None; self.binary_operators = None; self.operands = None; parser.toks_mut().set_cursor(self.start); self.allow_slash = true; self.single_expression = Some(self.parse_single_expression(parser)?); Ok(()) } } grass-0.13.4/crates/compiler/src/selector/000077500000000000000000000000001465374720000204305ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/selector/attribute.rs000066400000000000000000000165101465374720000230040ustar00rootroot00000000000000use std::{ fmt::{self, Display, Write}, hash::{Hash, Hasher}, }; use codemap::Span; use crate::{ common::QuoteKind, error::SassResult, parse::BaseParser, utils::is_ident, value::Value, Token, }; use super::{Namespace, QualifiedName, SelectorParser}; #[derive(Clone, Debug)] pub(crate) struct Attribute { attr: QualifiedName, value: String, modifier: Option, op: AttributeOp, span: Span, } impl PartialEq for Attribute { fn eq(&self, other: &Self) -> bool { self.attr == other.attr && self.value == other.value && self.modifier == other.modifier && self.op == other.op } } impl Eq for Attribute {} impl Hash for Attribute { fn hash(&self, state: &mut H) { self.attr.hash(state); self.value.hash(state); self.modifier.hash(state); self.op.hash(state); } } // todo: rewrite fn attribute_name(parser: &mut SelectorParser) -> SassResult { let next = parser .toks .peek() .ok_or_else(|| ("Expected identifier.", parser.toks.current_span()))?; if next.kind == '*' { parser.toks.next(); parser.expect_char('|')?; let ident = parser.parse_identifier(false, false)?; return Ok(QualifiedName { ident, namespace: Namespace::Asterisk, }); } let name_or_namespace = parser.parse_identifier(false, false)?; match parser.toks.peek() { Some(v) if v.kind != '|' => { return Ok(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }); } Some(..) => {} None => return Err(("expected more input.", parser.toks.current_span()).into()), } match parser.toks.peek_n(1) { Some(v) if v.kind == '=' => { return Ok(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }); } Some(..) => {} None => return Err(("expected more input.", parser.toks.current_span()).into()), } parser.toks.next(); let ident = parser.parse_identifier(false, false)?; Ok(QualifiedName { ident, namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } fn attribute_operator(parser: &mut SelectorParser) -> SassResult { let op = match parser.toks.next() { Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals), Some(Token { kind: '~', .. }) => AttributeOp::Include, Some(Token { kind: '|', .. }) => AttributeOp::Dash, Some(Token { kind: '^', .. }) => AttributeOp::Prefix, Some(Token { kind: '$', .. }) => AttributeOp::Suffix, Some(Token { kind: '*', .. }) => AttributeOp::Contains, Some(..) | None => return Err(("Expected \"]\".", parser.toks.current_span()).into()), }; parser.expect_char('=')?; Ok(op) } impl Attribute { pub fn from_tokens(parser: &mut SelectorParser) -> SassResult { let start = parser.toks.cursor(); parser.whitespace_without_comments(); let attr = attribute_name(parser)?; parser.whitespace_without_comments(); if parser .toks .peek() .ok_or_else(|| ("expected more input.", parser.toks.current_span()))? .kind == ']' { parser.toks.next(); return Ok(Attribute { attr, value: String::new(), modifier: None, op: AttributeOp::Any, span: parser.toks.span_from(start), }); } let op = attribute_operator(parser)?; parser.whitespace_without_comments(); let peek = parser .toks .peek() .ok_or_else(|| ("expected more input.", parser.toks.current_span()))?; let value = match peek.kind { '\'' | '"' => parser.parse_string()?, _ => parser.parse_identifier(false, false)?, }; parser.whitespace_without_comments(); let modifier = match parser.toks.peek() { Some(Token { kind: c @ 'a'..='z', .. }) | Some(Token { kind: c @ 'A'..='Z', .. }) => { parser.toks.next(); parser.whitespace_without_comments(); Some(c) } _ => None, }; parser.expect_char(']')?; Ok(Attribute { op, attr, value, modifier, span: parser.toks.span_from(start), }) } } impl Display for Attribute { #[allow(clippy::branches_sharing_code)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_char('[')?; write!(f, "{}", self.attr)?; if self.op != AttributeOp::Any { f.write_str(self.op.into())?; if is_ident(&self.value) && !self.value.starts_with("--") { f.write_str(&self.value)?; if self.modifier.is_some() { f.write_char(' ')?; } } else { // todo: remove unwrap by not doing this in display // or having special emitter for quoted strings? // (also avoids the clone because we can consume/modify self) f.write_str( &Value::String(self.value.clone(), QuoteKind::Quoted) .to_css_string(self.span, false) .unwrap(), )?; // todo: this space is not emitted when `compressed` output if self.modifier.is_some() { f.write_char(' ')?; } } if let Some(c) = self.modifier { f.write_char(c)?; } } f.write_char(']')?; Ok(()) } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] enum AttributeOp { /// \[attr\] /// /// Represents elements with an attribute name of `attr` Any, /// [attr=value] /// /// Represents elements with an attribute name of `attr` /// whose value is exactly `value` Equals, /// [attr~=value] /// /// Represents elements with an attribute name of `attr` /// whose value is a whitespace-separated list of words, /// one of which is exactly `value` Include, /// [attr|=value] /// /// Represents elements with an attribute name of `attr` /// whose value can be exactly value or can begin with /// `value` immediately followed by a hyphen (`-`) Dash, /// [attr^=value] Prefix, /// [attr$=value] Suffix, /// [attr*=value] /// /// Represents elements with an attribute name of `attr` /// whose value contains at least one occurrence of /// `value` within the string Contains, } impl From for &'static str { #[inline] fn from(op: AttributeOp) -> Self { match op { AttributeOp::Any => "", AttributeOp::Equals => "=", AttributeOp::Include => "~=", AttributeOp::Dash => "|=", AttributeOp::Prefix => "^=", AttributeOp::Suffix => "$=", AttributeOp::Contains => "*=", } } } grass-0.13.4/crates/compiler/src/selector/common.rs000066400000000000000000000024141465374720000222670ustar00rootroot00000000000000use std::fmt; /// The selector namespace. /// /// If this is `None`, this matches all elements in the default namespace. If /// it's `Empty`, this matches all elements that aren't in any /// namespace. If it's `Asterisk`, this matches all elements in any namespace. /// Otherwise, it matches all elements in the given namespace. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum Namespace { Empty, Asterisk, Other(Box), None, } impl fmt::Display for Namespace { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Empty => write!(f, "|"), Self::Asterisk => write!(f, "*|"), Self::Other(namespace) => write!(f, "{}|", namespace), Self::None => Ok(()), } } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct QualifiedName { pub ident: String, pub namespace: Namespace, } impl fmt::Display for QualifiedName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.namespace)?; f.write_str(&self.ident) } } pub(crate) struct Specificity { pub min: i32, pub max: i32, } impl Specificity { pub const fn new(min: i32, max: i32) -> Self { Specificity { min, max } } } grass-0.13.4/crates/compiler/src/selector/complex.rs000066400000000000000000000251621465374720000224530ustar00rootroot00000000000000use std::{ collections::HashSet, fmt::{self, Display, Write}, hash::{Hash, Hasher}, sync::atomic::{AtomicU32, Ordering as AtomicOrdering}, }; use codemap::Span; use crate::error::SassResult; use super::{CompoundSelector, Pseudo, SelectorList, SimpleSelector, Specificity}; pub(crate) static COMPLEX_SELECTOR_UNIQUE_ID: AtomicU32 = AtomicU32::new(0); #[derive(Clone, Debug)] pub(crate) struct ComplexSelectorHashSet(HashSet); impl ComplexSelectorHashSet { pub fn new() -> Self { Self(HashSet::new()) } pub fn insert(&mut self, complex: &ComplexSelector) -> bool { self.0.insert(complex.unique_id) } pub fn contains(&self, complex: &ComplexSelector) -> bool { self.0.contains(&complex.unique_id) } pub fn extend<'a>(&mut self, complexes: impl Iterator) { self.0.extend(complexes.map(|complex| complex.unique_id)); } } /// A complex selector. /// /// A complex selector is composed of `CompoundSelector`s separated by /// `Combinator`s. It selects elements based on their parent selectors. #[derive(Clone, Debug)] pub(crate) struct ComplexSelector { /// The components of this selector. /// /// This is never empty. /// /// Descendant combinators aren't explicitly represented here. If two /// `CompoundSelector`s are adjacent to one another, there's an implicit /// descendant combinator between them. /// /// It's possible for multiple `Combinator`s to be adjacent to one another. /// This isn't valid CSS, but Sass supports it for CSS hack purposes. pub components: Vec, /// Whether a line break should be emitted *before* this selector. pub line_break: bool, /// A unique identifier for this complex selector. Used to perform a pointer /// equality check, like would be done for objects in a language like JavaScript /// or dart unique_id: u32, } impl PartialEq for ComplexSelector { fn eq(&self, other: &Self) -> bool { self.components == other.components } } impl Eq for ComplexSelector {} impl Hash for ComplexSelector { fn hash(&self, state: &mut H) { self.components.hash(state); } } impl fmt::Display for ComplexSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut last_component = None; for component in &self.components { if let Some(c) = last_component { if !omit_spaces_around(c) && !omit_spaces_around(component) { f.write_char(' ')?; } } write!(f, "{}", component)?; last_component = Some(component); } Ok(()) } } /// When `style` is `OutputStyle::compressed`, omit spaces around combinators. fn omit_spaces_around(component: &ComplexSelectorComponent) -> bool { // todo: compressed let is_compressed = false; is_compressed && matches!(component, ComplexSelectorComponent::Combinator(..)) } impl ComplexSelector { pub fn new(components: Vec, line_break: bool) -> Self { Self { components, line_break, unique_id: COMPLEX_SELECTOR_UNIQUE_ID.fetch_add(1, AtomicOrdering::Relaxed), } } pub fn max_specificity(&self) -> i32 { self.specificity().min } pub fn min_specificity(&self) -> i32 { self.specificity().max } pub fn specificity(&self) -> Specificity { let mut min = 0; let mut max = 0; for component in &self.components { if let ComplexSelectorComponent::Compound(compound) = component { min += compound.min_specificity(); max += compound.max_specificity(); } } Specificity::new(min, max) } pub fn is_invisible(&self) -> bool { self.components .iter() .any(ComplexSelectorComponent::is_invisible) } /// Returns whether `self` is a superselector of `other`. /// /// That is, whether `self` matches every element that `other` matches, as well /// as possibly additional elements. pub fn is_super_selector(&self, other: &Self) -> bool { if let Some(ComplexSelectorComponent::Combinator(..)) = self.components.last() { return false; } if let Some(ComplexSelectorComponent::Combinator(..)) = other.components.last() { return false; } let mut i1 = 0; let mut i2 = 0; loop { let remaining1 = self.components.len() - i1; let remaining2 = other.components.len() - i2; if remaining1 == 0 || remaining2 == 0 || remaining1 > remaining2 { return false; } let compound1 = match self.components.get(i1) { Some(ComplexSelectorComponent::Compound(c)) => c, Some(ComplexSelectorComponent::Combinator(..)) => return false, None => unreachable!(), }; if let ComplexSelectorComponent::Combinator(..) = other.components[i2] { return false; } if remaining1 == 1 { let parents = other .components .iter() .take(other.components.len() - 1) .skip(i2) .cloned() .collect(); return compound1.is_super_selector( other.components.last().unwrap().as_compound(), &Some(parents), ); } let mut after_super_selector = i2 + 1; while after_super_selector < other.components.len() { if let Some(ComplexSelectorComponent::Compound(compound2)) = other.components.get(after_super_selector - 1) { if compound1.is_super_selector( compound2, &Some( other .components .iter() .take(after_super_selector - 1) .skip(i2 + 1) .cloned() .collect(), ), ) { break; } } after_super_selector += 1; } if after_super_selector == other.components.len() { return false; } if let Some(ComplexSelectorComponent::Combinator(combinator1)) = self.components.get(i1 + 1) { let combinator2 = match other.components.get(after_super_selector) { Some(ComplexSelectorComponent::Combinator(c)) => c, Some(ComplexSelectorComponent::Compound(..)) => return false, None => unreachable!(), }; if combinator1 == &Combinator::FollowingSibling { if combinator2 == &Combinator::Child { return false; } } else if combinator1 != combinator2 { return false; } if remaining1 == 3 && remaining2 > 3 { return false; } i1 += 2; i2 = after_super_selector + 1; } else if let Some(ComplexSelectorComponent::Combinator(combinator2)) = other.components.get(after_super_selector) { if combinator2 != &Combinator::Child { return false; } i1 += 1; i2 = after_super_selector + 1; } else { i1 += 1; i2 = after_super_selector; } } } pub fn contains_parent_selector(&self) -> bool { self.components.iter().any(|c| { if let ComplexSelectorComponent::Compound(compound) = c { compound.components.iter().any(|simple| { if simple.is_parent() { return true; } if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), .. }) = simple { return sel.contains_parent_selector(); } false }) } else { false } }) } } #[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)] pub(crate) enum Combinator { /// Matches the right-hand selector if it's immediately adjacent to the /// left-hand selector in the DOM tree. /// /// `'+'` NextSibling, /// Matches the right-hand selector if it's a direct child of the left-hand /// selector in the DOM tree. /// /// `'>'` Child, /// Matches the right-hand selector if it comes after the left-hand selector /// in the DOM tree. /// /// `'~'` FollowingSibling, } impl Display for Combinator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_char(match self { Self::NextSibling => '+', Self::Child => '>', Self::FollowingSibling => '~', }) } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum ComplexSelectorComponent { Combinator(Combinator), Compound(CompoundSelector), } impl ComplexSelectorComponent { pub fn is_invisible(&self) -> bool { match self { Self::Combinator(..) => false, Self::Compound(c) => c.is_invisible(), } } pub fn is_compound(&self) -> bool { matches!(self, Self::Compound(..)) } pub fn is_combinator(&self) -> bool { matches!(self, Self::Combinator(..)) } pub fn resolve_parent_selectors( self, span: Span, parent: SelectorList, ) -> SassResult>> { match self { Self::Compound(c) => c.resolve_parent_selectors(span, parent), Self::Combinator(..) => todo!(), } } pub fn as_compound(&self) -> &CompoundSelector { match self { Self::Compound(c) => c, Self::Combinator(..) => unreachable!(), } } } impl Display for ComplexSelectorComponent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Compound(c) => write!(f, "{}", c), Self::Combinator(c) => write!(f, "{}", c), } } } grass-0.13.4/crates/compiler/src/selector/compound.rs000066400000000000000000000174351465374720000226340ustar00rootroot00000000000000use std::fmt::{self, Write}; use codemap::Span; use crate::error::SassResult; use super::{ ComplexSelector, ComplexSelectorComponent, Namespace, Pseudo, SelectorList, SimpleSelector, Specificity, }; /// A compound selector is composed of several /// simple selectors #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct CompoundSelector { pub components: Vec, } impl fmt::Display for CompoundSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut did_write = false; for simple in &self.components { if did_write { write!(f, "{}", simple)?; } else { let s = simple.to_string(); if !s.is_empty() { did_write = true; } write!(f, "{}", s)?; } } // If we emit an empty compound, it's because all of the components got // optimized out because they match all selectors, so we just emit the // universal selector. if !did_write { f.write_char('*')?; } Ok(()) } } impl CompoundSelector { pub fn max_specificity(&self) -> i32 { self.specificity().max } pub fn min_specificity(&self) -> i32 { self.specificity().min } /// Returns tuple of (min, max) specificity pub fn specificity(&self) -> Specificity { let mut min = 0; let mut max = 0; for simple in &self.components { min += simple.min_specificity(); max += simple.max_specificity(); } Specificity::new(min, max) } pub fn is_invisible(&self) -> bool { self.components.iter().any(SimpleSelector::is_invisible) } pub fn is_super_selector( &self, other: &Self, parents: &Option>, ) -> bool { for simple1 in &self.components { if let SimpleSelector::Pseudo( pseudo @ Pseudo { selector: Some(..), .. }, ) = simple1 { if !pseudo.is_super_selector(other, parents.clone()) { return false; } } else if !simple1.is_super_selector_of_compound(other) { return false; } } for simple2 in &other.components { if let SimpleSelector::Pseudo(Pseudo { is_class: false, selector: None, .. }) = simple2 { if !simple2.is_super_selector_of_compound(self) { return false; } } } true } /// Returns a new `CompoundSelector` based on `compound` with all /// `SimpleSelector::Parent`s replaced with `parent`. /// /// Returns `None` if `compound` doesn't contain any `SimpleSelector::Parent`s. pub fn resolve_parent_selectors( self, span: Span, parent: SelectorList, ) -> SassResult>> { let contains_selector_pseudo = self.components.iter().any(|simple| { if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), .. }) = simple { sel.contains_parent_selector() } else { false } }); if !contains_selector_pseudo && !self.components[0].is_parent() { return Ok(None); } let resolved_members: Vec = if contains_selector_pseudo { self.components .clone() .into_iter() .map(|simple| { if let SimpleSelector::Pseudo(mut pseudo) = simple { if let Some(sel) = pseudo.selector.clone() { if !sel.contains_parent_selector() { return Ok(SimpleSelector::Pseudo(pseudo)); } pseudo.selector = Some(Box::new( sel.resolve_parent_selectors(Some(parent.clone()), false)?, )); } Ok(SimpleSelector::Pseudo(pseudo)) } else { Ok(simple) } }) .collect::>>()? } else { self.components.clone() }; if let Some(SimpleSelector::Parent(suffix)) = self.components.first() { if self.components.len() == 1 && suffix.is_none() { return Ok(Some(parent.components)); } } else { return Ok(Some(vec![ComplexSelector::new( vec![ComplexSelectorComponent::Compound(CompoundSelector { components: resolved_members, })], false, )])); } let parent_span = parent.span; Ok(Some( parent .components .into_iter() .map(move |mut complex| { let last_component = complex.components.last(); let last = if let Some(ComplexSelectorComponent::Compound(c)) = last_component { c.clone() } else { return Err(( format!("Parent \"{}\" is incompatible with this selector.", complex), span, ) .into()); }; let mut components = last.components; if let Some(SimpleSelector::Parent(Some(suffix))) = self.components.first() { let mut end = components.pop().unwrap(); end.add_suffix(suffix, parent_span)?; components.push(end); } components.extend(resolved_members.clone().into_iter().skip(1)); let last = CompoundSelector { components }; complex.components.pop(); let mut components = complex.components; components.push(ComplexSelectorComponent::Compound(last)); Ok(ComplexSelector::new(components, complex.line_break)) }) .collect::>>()?, )) } /// Returns a `CompoundSelector` that matches only elements that are matched by /// both `compound1` and `compound2`. /// /// If no such selector can be produced, returns `None`. pub fn unify(self, other: Self) -> Option { let mut components = other.components; for simple in self.components { components = simple.unify(std::mem::take(&mut components))?; } Some(Self { components }) } /// Adds a `SimpleSelector::Parent` to the beginning of `compound`, or returns `None` if /// that wouldn't produce a valid selector. pub fn prepend_parent(mut self) -> Option { Some(match self.components.first()? { SimpleSelector::Universal(..) => return None, SimpleSelector::Type(name) => { if name.namespace != Namespace::None { return None; } let mut components = vec![SimpleSelector::Parent(Some(name.ident.clone()))]; components.extend(self.components.into_iter().skip(1)); Self { components } } _ => { let mut components = vec![SimpleSelector::Parent(None)]; components.append(&mut self.components); Self { components } } }) } } grass-0.13.4/crates/compiler/src/selector/extend/000077500000000000000000000000001465374720000217175ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/selector/extend/extended_selector.rs000066400000000000000000000043011465374720000257630ustar00rootroot00000000000000use std::{ cell::RefCell, collections::{hash_set::IntoIter, HashSet}, hash::{Hash, Hasher}, ops::Deref, ptr, rc::Rc, }; use crate::selector::{Selector, SelectorList}; #[derive(Debug, Clone)] pub(crate) struct ExtendedSelector(Rc>); impl PartialEq for ExtendedSelector { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl Eq for ExtendedSelector {} impl Hash for ExtendedSelector { // We hash the ptr here for efficiency. // TODO: is this an issue? it probably is, // but I haven't managed to find a test case // that exhibits it. fn hash(&self, state: &mut H) { ptr::hash(&*self.0, state); // in case we need to hash the actual value: // self.0.borrow().hash(state); } } impl ExtendedSelector { pub fn new(selector: SelectorList) -> Self { Self(Rc::new(RefCell::new(selector))) } pub fn is_invisible(&self) -> bool { (*self.0).borrow().is_invisible() } pub fn into_selector(self) -> Selector { Selector(match Rc::try_unwrap(self.0) { Ok(v) => v.into_inner(), Err(v) => v.borrow().clone(), }) } pub fn as_selector_list(&self) -> impl Deref + '_ { self.0.borrow() } pub fn set_inner(&mut self, selector: SelectorList) { self.0.replace(selector); } } /// There is the potential for danger here by modifying the hash /// through `RefCell`, but I haven't come up with a good solution /// for this yet (we can't just use a `Vec` because linear insert) /// is too big of a penalty /// /// In practice, I have yet to find a test case that can demonstrate /// an issue with storing a `RefCell`. #[derive(Clone, Debug)] pub(crate) struct SelectorHashSet(HashSet); impl SelectorHashSet { pub fn new() -> Self { Self(HashSet::new()) } pub fn insert(&mut self, selector: ExtendedSelector) { self.0.insert(selector); } } impl IntoIterator for SelectorHashSet { type Item = ExtendedSelector; type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } grass-0.13.4/crates/compiler/src/selector/extend/extension.rs000066400000000000000000000046641465374720000243130ustar00rootroot00000000000000use codemap::Span; use crate::ast::CssMediaQuery; use super::{ComplexSelector, SimpleSelector}; #[derive(Clone, Debug)] pub(crate) struct Extension { /// The selector in which the `@extend` appeared. pub extender: ComplexSelector, /// The selector that's being extended. /// /// `None` for one-off extensions. pub target: Option, /// The minimum specificity required for any selector generated from this /// extender. pub specificity: i32, /// Whether this extension is optional. pub is_optional: bool, /// Whether this is a one-off extender representing a selector that was /// originally in the document, rather than one defined with `@extend`. pub is_original: bool, /// The media query context to which this extend is restricted, or `None` if /// it can apply within any context. pub media_context: Option>, /// The span in which `extender` was defined. pub span: Span, #[allow(dead_code)] pub left: Option>, #[allow(dead_code)] pub right: Option>, } impl Extension { pub fn one_off( extender: ComplexSelector, specificity: Option, is_original: bool, span: Span, ) -> Self { Self { specificity: specificity.unwrap_or_else(|| extender.max_specificity()), extender, target: None, span, is_optional: true, is_original, media_context: None, left: None, right: None, } } /// Asserts that the `media_context` for a selector is compatible with the /// query context for this extender. // todo: this should return a `Result`. it currently does not because the cascade effect // from this returning a `Result` will make some code returning `Option`s much uglier (we can't // use `?` to return both `Option` and `Result` from the same function) #[allow(clippy::needless_return)] pub fn assert_compatible_media_context(&self, media_context: &Option>) { if &self.media_context == media_context { return; } // Err(("You may not @extend selectors across media queries.", self.span).into()) } #[allow(clippy::missing_const_for_fn)] pub fn with_extender(mut self, extender: ComplexSelector) -> Self { self.extender = extender; self } } grass-0.13.4/crates/compiler/src/selector/extend/functions.rs000066400000000000000000000670041465374720000243040ustar00rootroot00000000000000#![allow(clippy::similar_names)] use std::collections::VecDeque; use super::super::{ Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SimpleSelector, }; /// Returns the contents of a `SelectorList` that matches only elements that are /// matched by both `complex_one` and `complex_two`. /// /// If no such list can be produced, returns `None`. pub(crate) fn unify_complex( complexes: Vec>, ) -> Option>> { debug_assert!(!complexes.is_empty()); if complexes.len() == 1 { return Some(complexes); } let mut unified_base: Option> = None; for complex in &complexes { let base = complex.last()?; if let ComplexSelectorComponent::Compound(base) = base { if let Some(mut some_unified_base) = unified_base.clone() { for simple in base.components.clone() { some_unified_base = simple.unify(some_unified_base.clone())?; } unified_base = Some(some_unified_base); } else { unified_base = Some(base.components.clone()); } } else { return None; } } let mut complexes_without_bases: Vec> = complexes .into_iter() .map(|mut complex| { complex.pop(); complex }) .collect(); complexes_without_bases .last_mut() .unwrap() .push(ComplexSelectorComponent::Compound(CompoundSelector { components: unified_base?, })); Some(weave(complexes_without_bases)) } /// Expands "parenthesized selectors" in `complexes`. /// /// That is, if we have `.A .B {@extend .C}` and `.D .C {...}`, this /// conceptually expands into `.D .C, .D (.A .B)`, and this function translates /// `.D (.A .B)` into `.D .A .B, .A .D .B`. For thoroughness, `.A.D .B` would /// also be required, but including merged selectors results in exponential /// output for very little gain. /// /// The selector `.D (.A .B)` is represented as the list `[[.D], [.A, .B]]`. pub(crate) fn weave( mut complexes: Vec>, ) -> Vec> { let mut prefixes: Vec> = vec![complexes.remove(0)]; for mut complex in complexes { let target = match complex.pop() { Some(c) => c, None => continue, }; if complex.is_empty() { for prefix in &mut prefixes { prefix.push(target.clone()); } continue; } let parents: Vec = complex; let mut new_prefixes: Vec> = Vec::new(); for prefix in prefixes { if let Some(parent_prefixes) = weave_parents(prefix, parents.clone()) { for mut parent_prefix in parent_prefixes { parent_prefix.push(target.clone()); new_prefixes.push(parent_prefix); } } } prefixes = new_prefixes; } prefixes } /// Interweaves `parents_one` and `parents_two` as parents of the same target selector. /// /// Returns all possible orderings of the selectors in the inputs (including /// using unification) that maintain the relative ordering of the input. For /// example, given `.foo .bar` and `.baz .bang`, this would return `.foo .bar /// .baz .bang`, `.foo .bar.baz .bang`, `.foo .baz .bar .bang`, `.foo .baz /// .bar.bang`, `.foo .baz .bang .bar`, and so on until `.baz .bang .foo .bar`. /// /// Semantically, for selectors A and B, this returns all selectors `AB_i` /// such that the union over all i of elements matched by `AB_i X` is /// identical to the intersection of all elements matched by `A X` and all /// elements matched by `B X`. Some `AB_i` are elided to reduce the size of /// the output. fn weave_parents( parents_one: Vec, parents_two: Vec, ) -> Option>> { let mut queue_one = VecDeque::from(parents_one); let mut queue_two = VecDeque::from(parents_two); let initial_combinators = merge_initial_combinators(&mut queue_one, &mut queue_two)?; let mut final_combinators = merge_final_combinators(&mut queue_one, &mut queue_two, None)?; match (first_if_root(&mut queue_one), first_if_root(&mut queue_two)) { (Some(root_one), Some(root_two)) => { let root = ComplexSelectorComponent::Compound(root_one.unify(root_two)?); queue_one.push_front(root.clone()); queue_two.push_front(root); } (Some(root_one), None) => { queue_two.push_front(ComplexSelectorComponent::Compound(root_one)); } (None, Some(root_two)) => { queue_one.push_front(ComplexSelectorComponent::Compound(root_two)); } (None, None) => {} } let mut groups_one = group_selectors(Vec::from(queue_one)); let mut groups_two = group_selectors(Vec::from(queue_two)); let lcs = longest_common_subsequence( groups_two.as_slices().0, groups_one.as_slices().0, Some(&|group_one, group_two| { if group_one == group_two { return Some(group_one); } if let ComplexSelectorComponent::Combinator(..) = group_one.first()? { return None; } if let ComplexSelectorComponent::Combinator(..) = group_two.first()? { return None; } if complex_is_parent_superselector(group_one.clone(), group_two.clone()) { return Some(group_two); } if complex_is_parent_superselector(group_two.clone(), group_one.clone()) { return Some(group_one); } if !must_unify(&group_one, &group_two) { return None; } let unified = unify_complex(vec![group_one, group_two])?; if unified.len() > 1 { return None; } unified.first().cloned() }), ); let mut choices = vec![vec![initial_combinators .into_iter() .map(ComplexSelectorComponent::Combinator) .collect::>()]]; for group in lcs { choices.push( chunks(&mut groups_one, &mut groups_two, |sequence| { complex_is_parent_superselector( match sequence.front() { Some(v) => v.clone(), None => return true, }, group.clone(), ) }) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ); choices.push(vec![group]); groups_one.pop_front(); groups_two.pop_front(); } choices.push( chunks(&mut groups_one, &mut groups_two, VecDeque::is_empty) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ); choices.append(&mut final_combinators); Some( paths( choices .into_iter() .filter(|choice| !choice.is_empty()) .collect(), ) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ) } /// Extracts leading `Combinator`s from `components_one` and `components_two` and /// merges them together into a single list of combinators. /// /// If there are no combinators to be merged, returns an empty list. If the /// combinators can't be merged, returns `None`. fn merge_initial_combinators( components_one: &mut VecDeque, components_two: &mut VecDeque, ) -> Option> { let mut combinators_one: Vec = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(c)) = components_one.front() { combinators_one.push(*c); components_one.pop_front(); } let mut combinators_two = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(c)) = components_two.front() { combinators_two.push(*c); components_two.pop_front(); } let lcs = longest_common_subsequence(&combinators_one, &combinators_two, None); if lcs == combinators_one { Some(combinators_two) } else if lcs == combinators_two { Some(combinators_one) } else { // If neither sequence of combinators is a subsequence of the other, they // cannot be merged successfully. None } } /// Returns the longest common subsequence between `list_one` and `list_two`. /// /// If there are more than one equally long common subsequence, returns the one /// which starts first in `list_one`. /// /// If `select` is passed, it's used to check equality between elements in each /// list. If it returns `None`, the elements are considered unequal; otherwise, /// it should return the element to include in the return value. fn longest_common_subsequence( list_one: &[T], list_two: &[T], select: Option<&dyn Fn(T, T) -> Option>, ) -> Vec { let select = select.unwrap_or(&|element_one, element_two| { if element_one == element_two { Some(element_one) } else { None } }); let mut lengths = vec![vec![0; list_two.len() + 1]; list_one.len() + 1]; let mut selections: Vec>> = vec![vec![None; list_two.len()]; list_one.len()]; for i in 0..list_one.len() { for j in 0..list_two.len() { let selection = select( list_one.get(i).unwrap().clone(), list_two.get(j).unwrap().clone(), ); selections[i][j] = selection.clone(); lengths[i + 1][j + 1] = if selection.is_none() { std::cmp::max(lengths[i + 1][j], lengths[i][j + 1]) } else { lengths[i][j] + 1 }; } } fn backtrack( i: isize, j: isize, lengths: Vec>, selections: &mut Vec>>, ) -> Vec { if i == -1 || j == -1 { return Vec::new(); } let selection = selections.get(i as usize).cloned().unwrap_or_default(); if let Some(Some(selection)) = selection.get(j as usize) { let mut tmp = backtrack(i - 1, j - 1, lengths, selections); tmp.push(selection.clone()); return tmp; } if lengths[(i + 1) as usize][j as usize] > lengths[i as usize][(j + 1) as usize] { backtrack(i, j - 1, lengths, selections) } else { backtrack(i - 1, j, lengths, selections) } } backtrack( (list_one.len() as isize).saturating_sub(1), (list_two.len() as isize).saturating_sub(1), lengths, &mut selections, ) } /// Extracts trailing `Combinator`s, and the selectors to which they apply, from /// `components_one` and `components_two` and merges them together into a single list. /// /// If there are no combinators to be merged, returns an empty list. If the /// sequences can't be merged, returns `None`. #[allow(clippy::cognitive_complexity)] fn merge_final_combinators( components_one: &mut VecDeque, components_two: &mut VecDeque, result: Option>>>, ) -> Option>>> { let mut result = result.unwrap_or_default(); if (components_one.is_empty() || !components_one.back().unwrap().is_combinator()) && (components_two.is_empty() || !components_two.back().unwrap().is_combinator()) { return Some(Vec::from(result)); } let mut combinators_one = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(combinator)) = components_one.get(components_one.len().saturating_sub(1)) { combinators_one.push(*combinator); components_one.pop_back(); } let mut combinators_two = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(combinator)) = components_two.get(components_two.len().saturating_sub(1)) { combinators_two.push(*combinator); components_two.pop_back(); } if combinators_one.len() > 1 || combinators_two.len() > 1 { // If there are multiple combinators, something hacky's going on. If one // is a supersequence of the other, use that, otherwise give up. let lcs = longest_common_subsequence(&combinators_one, &combinators_two, None); if lcs == combinators_one { result.push_front(vec![combinators_two .into_iter() .map(ComplexSelectorComponent::Combinator) .rev() .collect()]); } else if lcs == combinators_two { result.push_front(vec![combinators_one .into_iter() .map(ComplexSelectorComponent::Combinator) .rev() .collect()]); } else { return None; } return Some(Vec::from(result)); } let combinator_one = combinators_one.first(); let combinator_two = combinators_two.first(); // This code looks complicated, but it's actually just a bunch of special // cases for interactions between different combinators. match (combinator_one, combinator_two) { (Some(combinator_one), Some(combinator_two)) => { let compound_one = match components_one.pop_back() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!(), }; let compound_two = match components_two.pop_back() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!(), }; match (combinator_one, combinator_two) { (Combinator::FollowingSibling, Combinator::FollowingSibling) => { if compound_one.is_super_selector(&compound_two, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_two), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]]); } else if compound_two.is_super_selector(&compound_one, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_one), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]]); } else { let mut choices = vec![ vec![ ComplexSelectorComponent::Compound(compound_one.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(compound_two.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ], vec![ ComplexSelectorComponent::Compound(compound_two.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(compound_one.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ], ]; if let Some(unified) = compound_one.unify(compound_two) { choices.push(vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]); } result.push_front(choices); } } (Combinator::FollowingSibling, Combinator::NextSibling) | (Combinator::NextSibling, Combinator::FollowingSibling) => { let following_sibling_selector = if combinator_one == &Combinator::FollowingSibling { compound_one.clone() } else { compound_two.clone() }; let next_sibling_selector = if combinator_one == &Combinator::FollowingSibling { compound_two.clone() } else { compound_one.clone() }; if following_sibling_selector.is_super_selector(&next_sibling_selector, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(next_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]]); } else { let mut v = vec![vec![ ComplexSelectorComponent::Compound(following_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(next_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]]; if let Some(unified) = compound_one.unify(compound_two) { v.push(vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]); } result.push_front(v); } } (Combinator::Child, Combinator::NextSibling) | (Combinator::Child, Combinator::FollowingSibling) => { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_two), ComplexSelectorComponent::Combinator(*combinator_two), ]]); components_one.push_back(ComplexSelectorComponent::Compound(compound_one)); components_one .push_back(ComplexSelectorComponent::Combinator(Combinator::Child)); } (Combinator::NextSibling, Combinator::Child) | (Combinator::FollowingSibling, Combinator::Child) => { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_one), ComplexSelectorComponent::Combinator(*combinator_one), ]]); components_two.push_back(ComplexSelectorComponent::Compound(compound_two)); components_two .push_back(ComplexSelectorComponent::Combinator(Combinator::Child)); } (..) => { if combinator_one != combinator_two { return None; } let unified = compound_one.unify(compound_two)?; result.push_front(vec![vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(*combinator_one), ]]); } } merge_final_combinators(components_one, components_two, Some(result)) } (Some(combinator_one), None) => { if *combinator_one == Combinator::Child && !components_two.is_empty() { if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() { if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() { if c2.is_super_selector(c1, &None) { components_two.pop_back(); } } } } result.push_front(vec![vec![ components_one.pop_back().unwrap(), ComplexSelectorComponent::Combinator(*combinator_one), ]]); merge_final_combinators(components_one, components_two, Some(result)) } (None, Some(combinator_two)) => { if *combinator_two == Combinator::Child && !components_one.is_empty() { if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() { if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() { if c1.is_super_selector(c2, &None) { components_one.pop_back(); } } } } result.push_front(vec![vec![ components_two.pop_back().unwrap(), ComplexSelectorComponent::Combinator(*combinator_two), ]]); merge_final_combinators(components_one, components_two, Some(result)) } (None, None) => unreachable!(), } } /// If the first element of `queue` has a `::root` selector, removes and returns /// that element. fn first_if_root(queue: &mut VecDeque) -> Option { if queue.is_empty() { return None; } if let Some(ComplexSelectorComponent::Compound(c)) = queue.front() { if !has_root(c) { return None; } let compound = c.clone(); queue.pop_front(); Some(compound) } else { None } } /// Returns whether or not `compound` contains a `::root` selector. fn has_root(compound: &CompoundSelector) -> bool { compound.components.iter().any(|simple| { if let SimpleSelector::Pseudo(pseudo) = simple { pseudo.is_class && pseudo.normalized_name() == "root" } else { false } }) } /// Returns `complex`, grouped into sub-lists such that no sub-list contains two /// adjacent `ComplexSelector`s. /// /// For example, `(A B > C D + E ~ > G)` is grouped into /// `[(A) (B > C) (D + E ~ > G)]`. fn group_selectors( complex: Vec, ) -> VecDeque> { let mut groups = VecDeque::new(); let mut iter = complex.into_iter(); groups.push_back(if let Some(c) = iter.next() { vec![c] } else { return groups; }); for c in iter { let mut last_group = groups.pop_back().unwrap(); if last_group .last() .map_or(false, ComplexSelectorComponent::is_combinator) || c.is_combinator() { last_group.push(c); groups.push_back(last_group); } else { groups.push_back(last_group); groups.push_back(vec![c]); } } groups } /// Returns all orderings of initial subseqeuences of `queue_one` and `queue_two`. /// /// The `done` callback is used to determine the extent of the initial /// subsequences. It's called with each queue until it returns `true`. /// /// This destructively removes the initial subsequences of `queue_one` and /// `queue_two`. /// /// For example, given `(A B C | D E)` and `(1 2 | 3 4 5)` (with `|` denoting /// the boundary of the initial subsequence), this would return `[(A B C 1 2), /// (1 2 A B C)]`. The queues would then contain `(D E)` and `(3 4 5)`. fn chunks( queue_one: &mut VecDeque, queue_two: &mut VecDeque, done: impl Fn(&VecDeque) -> bool, ) -> Vec> { let mut chunk_one = Vec::new(); while !done(queue_one) { chunk_one.push(queue_one.pop_front().unwrap()); } let mut chunk_two = Vec::new(); while !done(queue_two) { chunk_two.push(queue_two.pop_front().unwrap()); } match (chunk_one.is_empty(), chunk_two.is_empty()) { (true, true) => Vec::new(), (true, false) => vec![chunk_two], (false, true) => vec![chunk_one], (false, false) => { let mut l1 = chunk_one.clone(); l1.append(&mut chunk_two.clone()); let mut l2 = chunk_two; l2.append(&mut chunk_one); vec![l1, l2] } } } /// Like `complex_is_superselector`, but compares `complex_one` and `complex_two` as /// though they shared an implicit base `SimpleSelector`. /// /// For example, `B` is not normally a superselector of `B A`, since it doesn't /// match elements that match `A`. However, it *is* a parent superselector, /// since `B X` is a superselector of `B A X`. fn complex_is_parent_superselector( mut complex_one: Vec, mut complex_two: Vec, ) -> bool { if let Some(ComplexSelectorComponent::Combinator(..)) = complex_one.first() { return false; } if let Some(ComplexSelectorComponent::Combinator(..)) = complex_two.first() { return false; } if complex_one.len() > complex_two.len() { return false; } let base = CompoundSelector { components: vec![SimpleSelector::Placeholder(String::new())], }; complex_one.push(ComplexSelectorComponent::Compound(base.clone())); complex_two.push(ComplexSelectorComponent::Compound(base)); ComplexSelector::new(complex_one, false) .is_super_selector(&ComplexSelector::new(complex_two, false)) } /// Returns a list of all possible paths through the given lists. /// /// For example, given `[[1, 2], [3, 4], [5]]`, this returns: /// /// ```no_run /// [[1, 3, 5], /// [2, 3, 5], /// [1, 4, 5], /// [2, 4, 5]]; /// ``` pub(crate) fn paths(choices: Vec>) -> Vec> { choices.into_iter().fold(vec![vec![]], |paths, choice| { choice .into_iter() .flat_map(move |option| { paths.clone().into_iter().map(move |mut path| { path.push(option.clone()); path }) }) .collect() }) } /// Returns whether `complex_one` and `complex_two` need to be unified to produce a /// valid combined selector. /// /// This is necessary when both selectors contain the same unique simple /// selector, such as an ID. fn must_unify( complex_one: &[ComplexSelectorComponent], complex_two: &[ComplexSelectorComponent], ) -> bool { let mut unique_selectors = Vec::new(); for component in complex_one { if let ComplexSelectorComponent::Compound(c) = component { unique_selectors.extend(c.components.iter().filter(|f| is_unique(f))); } } if unique_selectors.is_empty() { return false; } complex_two.iter().any(|component| { if let ComplexSelectorComponent::Compound(compound) = component { compound .components .iter() .any(|simple| is_unique(simple) && unique_selectors.contains(&simple)) } else { false } }) } /// Returns whether a `CompoundSelector` may contain only one simple selector of /// the same type as `simple`. fn is_unique(simple: &SimpleSelector) -> bool { matches!( simple, SimpleSelector::Id(..) | SimpleSelector::Pseudo(Pseudo { is_class: false, .. }) ) } grass-0.13.4/crates/compiler/src/selector/extend/merged.rs000066400000000000000000000041521465374720000235320ustar00rootroot00000000000000use crate::error::SassResult; use super::Extension; /// An `Extension` created by merging two `Extension`s with the same extender /// and target. /// /// This is used when multiple mandatory extensions exist to ensure that both of /// them are marked as resolved. pub(super) struct MergedExtension; impl MergedExtension { /// Returns an extension that combines `left` and `right`. /// /// Returns an `Err` if `left` and `right` have incompatible media /// contexts. /// /// Returns an `Err` if `left` and `right` don't have the same /// extender and target. pub fn merge(left: Extension, right: Extension) -> SassResult { if left.extender != right.extender || left.target != right.target { return Err(( format!( "{} and {} aren't the same extension.", left.extender, right.extender ), left.span.merge(right.span), ) .into()); } if left.media_context.is_some() && right.media_context.is_some() && left.media_context != right.media_context { return Err(( "You may not @extend the same selector from within different media queries.", right.span, ) .into()); } if right.is_optional && right.media_context.is_none() { return Ok(left); } if left.is_optional && left.media_context.is_none() { return Ok(right); } Ok(MergedExtension::into_extension(left, right)) } fn into_extension(left: Extension, right: Extension) -> Extension { Extension { extender: left.extender, target: left.target, span: left.span, media_context: match left.media_context { Some(v) => Some(v), None => right.media_context, }, specificity: left.specificity, is_optional: true, is_original: false, left: None, right: None, } } } grass-0.13.4/crates/compiler/src/selector/extend/mod.rs000066400000000000000000001322001465374720000230420ustar00rootroot00000000000000use std::{ collections::{HashMap, HashSet, VecDeque}, hash::Hash, }; use codemap::Span; use indexmap::IndexMap; use crate::{ast::CssMediaQuery, error::SassResult}; use super::{ ComplexSelector, ComplexSelectorComponent, ComplexSelectorHashSet, CompoundSelector, Pseudo, SelectorList, SimpleSelector, }; pub(crate) use extended_selector::ExtendedSelector; use extended_selector::SelectorHashSet; use extension::Extension; pub(crate) use functions::unify_complex; use functions::{paths, weave}; use merged::MergedExtension; pub(crate) use rule::ExtendRule; mod extended_selector; mod extension; mod functions; mod merged; mod rule; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] /// Different modes in which extension can run. enum ExtendMode { /// Normal mode, used with the `@extend` rule. /// /// This preserves existing selectors and extends each target individually. Normal, /// Replace mode, used by the `selector-replace()` function. /// /// This replaces existing selectors and requires every target to match to /// extend a given compound selector. Replace, /// All-targets mode, used by the `selector-extend()` function. /// /// This preserves existing selectors but requires every target to match to /// extend a given compound selector. AllTargets, } impl Default for ExtendMode { fn default() -> Self { Self::Normal } } #[derive(Clone, Debug)] pub(crate) struct ExtensionStore { /// A map from all simple selectors in the stylesheet to the selector lists /// that contain them. /// /// This is used to find which selectors an `@extend` applies to and adjust /// them. selectors: HashMap, /// A map from all extended simple selectors to the sources of those /// extensions. extensions: HashMap>, /// A map from all simple selectors in extenders to the extensions that those /// extenders define. extensions_by_extender: HashMap>, /// A map from CSS selectors to the media query contexts they're defined in. /// /// This tracks the contexts in which each selector's style rule is defined. /// If a rule is defined at the top level, it doesn't have an entry. media_contexts: HashMap>, /// A map from `SimpleSelector`s to the specificity of their source /// selectors. /// /// This tracks the maximum specificity of the `ComplexSelector` that /// originally contained each `SimpleSelector`. This allows us to ensure that /// we don't trim any selectors that need to exist to satisfy the [second law /// of extend][]. /// /// [second law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 source_specificity: HashMap, /// A set of `ComplexSelector`s that were originally part of /// their component `SelectorList`s, as opposed to being added by `@extend`. /// /// This allows us to ensure that we don't trim any selectors that need to /// exist to satisfy the [first law of extend][]. /// /// [first law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 originals: ComplexSelectorHashSet, /// The mode that controls this extender's behavior. mode: ExtendMode, span: Span, } impl ExtensionStore { /// An `Extender` that contains no extensions and can have no extensions added. // TODO: empty extender #[allow(dead_code)] const EMPTY: () = (); pub fn extend( selector: SelectorList, source: SelectorList, targets: SelectorList, span: Span, ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::AllTargets, span) } pub fn new(span: Span) -> Self { Self { selectors: HashMap::new(), extensions: HashMap::new(), extensions_by_extender: HashMap::new(), media_contexts: HashMap::new(), source_specificity: HashMap::new(), originals: ComplexSelectorHashSet::new(), mode: ExtendMode::Normal, span, } } pub fn replace( selector: SelectorList, source: SelectorList, targets: SelectorList, span: Span, ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::Replace, span) } fn extend_or_replace( selector: SelectorList, source: SelectorList, targets: SelectorList, mode: ExtendMode, span: Span, ) -> SassResult { let extenders: IndexMap = source .components .into_iter() .map(|complex| { ( complex.clone(), Extension::one_off(complex, None, false, span), ) }) .collect(); let compound_targets = targets .components .into_iter() .map(|complex| { if complex.components.len() == 1 { Ok(complex.components.first().unwrap().as_compound().clone()) } else { Err((format!("Can't extend complex selector {}.", complex), span).into()) } }) .collect::>>()?; let extensions: HashMap> = compound_targets .into_iter() .flat_map(|compound| { compound .components .into_iter() .map(|simple| (simple, extenders.clone())) }) .collect(); let mut extender = ExtensionStore::with_mode(mode, span); if !selector.is_invisible() { extender.originals.extend(selector.components.iter()); } Ok(extender.extend_list(selector, Some(&extensions), &None)) } fn with_mode(mode: ExtendMode, span: Span) -> Self { Self { mode, ..ExtensionStore::new(span) } } /// Extends `list` using `extensions`. fn extend_list( &mut self, list: SelectorList, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> SelectorList { // This could be written more simply using Vec>, but we want to avoid // any allocations in the common case where no extends apply. let mut extended: Option> = None; for (i, complex) in list.components.iter().enumerate() { if let Some(result) = self.extend_complex(complex.clone(), extensions, media_query_context) { if extended.is_none() { extended = Some(if i == 0 { Vec::new() } else { list.components[0..i].to_vec() }); } match extended.as_mut() { Some(v) => v.extend(result.into_iter()), None => unreachable!(), } } else if let Some(extended) = extended.as_mut() { extended.push(complex.clone()); } } let extended = match extended { Some(v) => v, None => return list, }; SelectorList { components: self.trim(extended, &|complex| self.originals.contains(complex)), span: self.span, } } /// Extends `complex` using `extensions`, and returns the contents of a /// `SelectorList`. fn extend_complex( &mut self, complex: ComplexSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> Option> { // The complex selectors that each compound selector in `complex.components` // can expand to. // // For example, given // // .a .b {...} // .x .y {@extend .b} // // this will contain // // [ // [.a], // [.b, .x .y] // ] // // This could be written more simply using `Vec::into_iter::map`, but we want to avoid // any allocations in the common case where no extends apply. let mut extended_not_expanded: Option>> = None; let complex_has_line_break = complex.line_break; let is_original = self.originals.contains(&complex); for (i, component) in complex.components.iter().enumerate() { if let ComplexSelectorComponent::Compound(component) = component { if let Some(extended) = self.extend_compound(component, extensions, media_query_context, is_original) { if extended_not_expanded.is_none() { extended_not_expanded = Some( complex .components .clone() .into_iter() .take(i) .map(|component| { vec![ComplexSelector::new(vec![component], complex.line_break)] }) .collect(), ); } match extended_not_expanded.as_mut() { Some(v) => v.push(extended), None => unreachable!(), } } else { match extended_not_expanded.as_mut() { Some(v) => v.push(vec![ComplexSelector::new( vec![ComplexSelectorComponent::Compound(component.clone())], false, )]), None => {} } } } else if component.is_combinator() { match extended_not_expanded.as_mut() { Some(v) => v.push(vec![ComplexSelector::new(vec![component.clone()], false)]), None => {} } } } let extended_not_expanded = extended_not_expanded?; let mut first = true; Some( paths(extended_not_expanded) .into_iter() .flat_map(move |path| { weave( path.clone() .into_iter() .map(move |complex| complex.components) .collect(), ) .into_iter() .map(|components| { let output_complex = ComplexSelector::new( components, complex_has_line_break || path.iter().any(|input_complex| input_complex.line_break), ); // Make sure that copies of `complex` retain their status as "original" // selectors. This includes selectors that are modified because a :not() // was extended into. if first && self.originals.contains(&complex) { self.originals.insert(&output_complex); } first = false; output_complex }) .collect::>() }) .collect(), ) } /// Extends `compound` using `extensions`, and returns the contents of a /// `SelectorList`. /// /// The `in_original` parameter indicates whether this is in an original /// complex selector, meaning that `compound` should not be trimmed out. fn extend_compound( &mut self, compound: &CompoundSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, in_original: bool, ) -> Option> { // If there's more than one target and they all need to match, we track // which targets are actually extended. let mut targets_used: HashSet = HashSet::new(); let mut options: Option>> = None; for i in 0..compound.components.len() { let simple = compound.components.get(i).cloned().unwrap(); match self.extend_simple( simple.clone(), extensions, media_query_context, &mut targets_used, ) { Some(extended) => { if options.is_none() { let mut new_options = Vec::new(); if i != 0 { new_options.push(vec![ self.extension_for_compound(compound.components[..i].to_vec()) ]); } options.replace(new_options); } match options.as_mut() { Some(v) => v.extend(extended.into_iter()), None => unreachable!(), } } None => match options.as_mut() { Some(v) => v.push(vec![self.extension_for_simple(simple)]), None => {} }, } } let options = options?; // If `self.mode` isn't `ExtendMode::Normal` and we didn't use all the targets in // `extensions`, extension fails for `compound`. // todo: test for `extensions.len() > 2`. may cause issues if !targets_used.is_empty() && targets_used.len() != extensions.map_or(self.extensions.len(), HashMap::len) && self.mode != ExtendMode::Normal { return None; } // Optimize for the simple case of a single simple selector that doesn't // need any unification. if options.len() == 1 { return Some( options .first()? .clone() .into_iter() .map(|state| { state.assert_compatible_media_context(media_query_context); state.extender }) .collect(), ); } // Find all paths through `options`. In this case, each path represents a // different unification of the base selector. For example, if we have: // // .a.b {...} // .w .x {@extend .a} // .y .z {@extend .b} // // then `options` is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is // // [ // [.a, .b], // [.a, .y .z], // [.w .x, .b], // [.w .x, .y .z] // ] // // We then unify each path to get a list of complex selectors: // // [ // [.a.b], // [.y .a.z], // [.w .x.b], // [.w .y .x.z, .y .w .x.z] // ] let mut first = self.mode != ExtendMode::Replace; let unified_paths = paths(options).into_iter().map(|path| { let complexes: Vec> = if first { // The first path is always the original selector. We can't just // return `compound` directly because pseudo selectors may be // modified, but we don't have to do any unification. first = false; vec![vec![ComplexSelectorComponent::Compound(CompoundSelector { components: path .clone() .into_iter() .flat_map(|state| { debug_assert!(state.extender.components.len() == 1); match state.extender.components.last().cloned() { Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(..) | None => unreachable!(), } }) .collect(), })]] } else { let mut to_unify: VecDeque> = VecDeque::new(); let mut originals: Vec = Vec::new(); for state in path.clone() { if state.is_original { originals.extend(match state.extender.components.last().cloned() { Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(..) | None => unreachable!(), }); } else { to_unify.push_back(state.extender.components.clone()); } } if !originals.is_empty() { to_unify.push_front(vec![ComplexSelectorComponent::Compound( CompoundSelector { components: originals, }, )]); } unify_complex(Vec::from(to_unify))? }; let mut line_break = false; for state in path { state.assert_compatible_media_context(media_query_context); line_break = line_break || state.extender.line_break; } Some( complexes .into_iter() .map(|components| ComplexSelector::new(components, line_break)) .collect::>(), ) }); let unified_paths: Vec = unified_paths.flatten().flatten().collect(); Some(if in_original && self.mode != ExtendMode::Replace { let original = unified_paths.first().cloned(); self.trim(unified_paths, &|complex| Some(complex) == original.as_ref()) } else { self.trim(unified_paths, &|_| false) }) } fn extend_simple( &mut self, simple: SimpleSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, targets_used: &mut HashSet, ) -> Option>> { if let SimpleSelector::Pseudo(Pseudo { selector: Some(..), .. }) = &simple { let simple = if let SimpleSelector::Pseudo(pseudo) = simple.clone() { pseudo } else { unreachable!() }; if let Some(extended) = self.extend_pseudo(simple, extensions, media_query_context) { return Some( extended .into_iter() .map(move |pseudo| { self.without_pseudo( SimpleSelector::Pseudo(pseudo.clone()), extensions, targets_used, self.mode, ) .unwrap_or_else(|| { vec![self.extension_for_simple(SimpleSelector::Pseudo(pseudo))] }) }) .collect(), ); } } self.without_pseudo(simple, extensions, targets_used, self.mode) .map(|v| vec![v]) } /// Extends `pseudo` using `extensions`, and returns a list of resulting /// pseudo selectors. fn extend_pseudo( &mut self, pseudo: Pseudo, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> Option> { let extended = self.extend_list( pseudo .selector .as_deref() .cloned() .unwrap_or_else(|| SelectorList::new(self.span)), extensions, media_query_context, ); /*todo: identical(extended, pseudo.selector)*/ if Some(&extended) == pseudo.selector.as_deref() { return None; } // For `:not()`, we usually want to get rid of any complex selectors because // that will cause the selector to fail to parse on all browsers at time of // writing. We can keep them if either the original selector had a complex // selector, or the result of extending has only complex selectors, because // either way we aren't breaking anything that isn't already broken. let mut complexes = if pseudo.normalized_name() == "not" && !pseudo .selector .clone() .unwrap() .components .iter() .any(|complex| complex.components.len() > 1) && extended .components .iter() .any(|complex| complex.components.len() == 1) { extended .components .into_iter() .filter(|complex| complex.components.len() <= 1) .collect() } else { extended.components }; complexes = complexes .into_iter() .flat_map(|complex| { if complex.components.len() != 1 { return vec![complex]; } let compound = match complex.components.first() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => return vec![complex], }; if compound.components.len() != 1 { return vec![complex]; } if !compound.components.first().unwrap().is_pseudo() { return vec![complex]; } let inner_pseudo = match compound.components.first() { Some(SimpleSelector::Pseudo(pseudo)) => pseudo, Some(..) | None => return vec![complex], }; if inner_pseudo.selector.is_none() { return vec![complex]; } match pseudo.normalized_name() { "not" => { // In theory, if there's a `:not` nested within another `:not`, the // inner `:not`'s contents should be unified with the return value. // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should // become `.foo:not(.bar)`. However, this is a narrow edge case and // supporting it properly would make this code and the code calling it // a lot more complicated, so it's not supported for now. let inner_pseudo_normalized = inner_pseudo.normalized_name(); if ["matches", "is", "where"].contains(&inner_pseudo_normalized) { inner_pseudo.selector.clone().unwrap().components } else { Vec::new() } } "matches" | "where" | "is" | "any" | "current" | "nth-child" | "nth-last-child" => { // As above, we could theoretically support :not within :matches, but // doing so would require this method and its callers to handle much // more complex cases that likely aren't worth the pain. if inner_pseudo.name != pseudo.name || inner_pseudo.argument != pseudo.argument { Vec::new() } else { inner_pseudo.selector.clone().unwrap().components } } "has" | "host" | "host-context" | "slotted" => { // We can't expand nested selectors here, because each layer adds an // additional layer of semantics. For example, `:has(:has(img))` // doesn't match `
` but `:has(img)` does. vec![complex] } _ => Vec::new(), } }) .collect(); // Older browsers support `:not`, but only with a single complex selector. // In order to support those browsers, we break up the contents of a `:not` // unless it originally contained a selector list. if pseudo.normalized_name() == "not" && pseudo.selector.clone().unwrap().components.len() == 1 { let result = complexes .into_iter() .map(|complex| { pseudo.clone().with_selector(Some(Box::new(SelectorList { components: vec![complex], span: self.span, }))) }) .collect::>(); if result.is_empty() { None } else { Some(result) } } else { Some(vec![pseudo.with_selector(Some(Box::new(SelectorList { components: complexes, span: self.span, })))]) } } /// Extends `simple` without extending the contents of any selector pseudos /// it contains. fn without_pseudo( &self, simple: SimpleSelector, extensions: Option<&HashMap>>, targets_used: &mut HashSet, mode: ExtendMode, ) -> Option> { let extenders = extensions.unwrap_or(&self.extensions).get(&simple)?; targets_used.insert(simple.clone()); if mode == ExtendMode::Replace { return Some(extenders.values().cloned().collect()); } let mut tmp = vec![self.extension_for_simple(simple)]; tmp.reserve(extenders.len()); tmp.extend(extenders.values().cloned()); Some(tmp) } /// Returns a one-off `Extension` whose extender is composed solely of /// `simple`. fn extension_for_simple(&self, simple: SimpleSelector) -> Extension { let specificity = Some(*self.source_specificity.get(&simple).unwrap_or(&0_i32)); Extension::one_off( ComplexSelector::new( vec![ComplexSelectorComponent::Compound(CompoundSelector { components: vec![simple], })], false, ), specificity, true, self.span, ) } /// Returns a one-off `Extension` whose extender is composed solely of a /// compound selector containing `simples`. fn extension_for_compound(&self, simples: Vec) -> Extension { let compound = CompoundSelector { components: simples, }; let specificity = Some(self.source_specificity_for(&compound)); Extension::one_off( ComplexSelector::new(vec![ComplexSelectorComponent::Compound(compound)], false), specificity, true, self.span, ) } /// Returns the maximum specificity for sources that went into producing /// `compound`. fn source_specificity_for(&self, compound: &CompoundSelector) -> i32 { let mut specificity = 0; for simple in &compound.components { specificity = specificity.max(*self.source_specificity.get(simple).unwrap_or(&0)); } specificity } /// Removes elements from `selectors` if they're subselectors of other /// elements. /// /// The `is_original` callback indicates which selectors are original to the /// document, and thus should never be trimmed. fn trim( &self, selectors: Vec, is_original: &dyn Fn(&ComplexSelector) -> bool, ) -> Vec { // Avoid truly horrific quadratic behavior. // // TODO(nweiz): I think there may be a way to get perfect trimming without // going quadratic by building some sort of trie-like data structure that // can be used to look up superselectors. if selectors.len() > 100 { return selectors; } // This is n² on the sequences, but only comparing between separate // sequences should limit the quadratic behavior. We iterate from last to // first and reverse the result so that, if two selectors are identical, we // keep the first one. let mut result: VecDeque = VecDeque::new(); let mut num_originals = 0; // :outer for i in (0..=(selectors.len().saturating_sub(1))).rev() { let mut should_continue_to_outer = false; let complex1 = selectors.get(i).unwrap(); if is_original(complex1) { // Make sure we don't include duplicate originals, which could happen if // a style rule extends a component of its own selector. for j in 0..num_originals { if result.get(j) == Some(complex1) { rotate_slice(&mut result, 0, j + 1); should_continue_to_outer = true; break; } } if should_continue_to_outer { continue; } num_originals += 1; result.push_front(complex1.clone()); continue; } // The maximum specificity of the sources that caused `complex1` to be // generated. In order for `complex1` to be removed, there must be another // selector that's a superselector of it *and* that has specificity // greater or equal to this. let mut max_specificity = 0; for component in &complex1.components { if let ComplexSelectorComponent::Compound(compound) = component { max_specificity = max_specificity.max(self.source_specificity_for(compound)); } } // Look in `result` rather than `selectors` for selectors after `i`. This // ensures that we aren't comparing against a selector that's already been // trimmed, and thus that if there are two identical selectors only one is // trimmed. let should_continue = result.iter().any(|complex2| { complex2.min_specificity() >= max_specificity && complex2.is_super_selector(complex1) }); if should_continue { continue; } let should_continue = selectors.iter().take(i).any(|complex2| { complex2.min_specificity() >= max_specificity && complex2.is_super_selector(complex1) }); if should_continue { continue; } result.push_front(complex1.clone()); } Vec::from(result) } /// Adds `selector` to this extender. /// /// Extends `selector` using any registered extensions, then returns the resulting /// selector. If any more relevant extensions are added, the returned selector /// is automatically updated. /// /// The `media_query_context` is the media query context in which the selector was /// defined, or `None` if it was defined at the top level of the document. pub fn add_selector( &mut self, mut selector: SelectorList, // span: Span, media_query_context: &Option>, ) -> ExtendedSelector { if !selector.is_invisible() { for complex in selector.components.clone() { self.originals.insert(&complex); } } if !self.extensions.is_empty() { selector = self.extend_list(selector, None, media_query_context); /* todo: when we have error handling } on SassException catch (error) { throw SassException( "From ${error.span.message('')}\n" "${error.message}", span); } */ } if let Some(mut media_query_context) = media_query_context.clone() { self.media_contexts .get_mut(&selector) .replace(&mut media_query_context); } let extended_selector = ExtendedSelector::new(selector.clone()); self.register_selector(selector, &extended_selector); extended_selector } /// Registers the `SimpleSelector`s in `list` to point to `selector` in /// `self.selectors`. fn register_selector(&mut self, list: SelectorList, selector: &ExtendedSelector) { for complex in list.components { for component in complex.components { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { // PERF: we compute the hash twice, which isn't great, but we avoid a superfluous // clone in cases where we have already seen a simple selector (common in // scenarios in which there is a lot of nesting) if let Some(entry) = self.selectors.get_mut(&simple) { entry.insert(selector.clone()); } else { self.selectors .entry(simple.clone()) .or_insert_with(SelectorHashSet::new) .insert(selector.clone()); } if let SimpleSelector::Pseudo(Pseudo { selector: Some(simple_selector), .. }) = simple { self.register_selector(*simple_selector, selector); } } } } } } /// Adds an extension to this extender. /// /// The `extender` is the selector for the style rule in which the extension /// is defined, and `target` is the selector passed to `@extend`. The `extend` /// provides the extend span and indicates whether the extension is optional. /// /// The `media_context` defines the media query context in which the extension /// is defined. It can only extend selectors within the same context. A `None` /// context indicates no media queries. pub fn add_extension( &mut self, extender: SelectorList, target: &SimpleSelector, extend: &ExtendRule, media_context: &Option>, span: Span, ) { let selectors = self.selectors.get(target).cloned(); let existing_extensions = self.extensions_by_extender.get(target).cloned(); let mut new_extensions: Option> = None; for complex in extender.components { let state = Extension { specificity: complex.max_specificity(), extender: complex.clone(), target: Some(target.clone()), span, media_context: media_context.clone(), is_optional: extend.is_optional, is_original: false, left: None, right: None, }; let sources = self .extensions .entry(target.clone()) .or_insert_with(IndexMap::new); if let Some(existing_state) = sources.get(&complex) { // If there's already an extend from `extender` to `target`, we don't need // to re-run the extension. We may need to mark the extension as // mandatory, though. let mut new_val = MergedExtension::merge(existing_state.clone(), state).unwrap(); sources.get_mut(&complex).replace(&mut new_val); continue; } sources.insert(complex.clone(), state.clone()); for component in complex.components.clone() { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { self.extensions_by_extender .entry(simple.clone()) .or_insert_with(Vec::new) .push(state.clone()); // Only source specificity for the original selector is relevant. // Selectors generated by `@extend` don't get new specificity. self.source_specificity .entry(simple.clone()) .or_insert_with(|| complex.max_specificity()); } } } if selectors.is_some() || existing_extensions.is_some() { new_extensions .get_or_insert_with(IndexMap::new) .insert(complex.clone(), state.clone()); } } let new_extensions = if let Some(new) = new_extensions { new } else { return; }; let mut new_extensions_by_target = HashMap::new(); new_extensions_by_target.insert(target.clone(), new_extensions); if let Some(existing_extensions) = existing_extensions { let additional_extensions = self.extend_existing_extensions(existing_extensions, &new_extensions_by_target); if let Some(additional_extensions) = additional_extensions { map_add_all_2(&mut new_extensions_by_target, additional_extensions); } } if let Some(selectors) = selectors { self.extend_existing_selectors(selectors, &new_extensions_by_target); } } /// Extend `extensions` using `new_extensions`. /// /// Note that this does duplicate some work done by /// `Extender::extend_existing_selectors`, but it's necessary to expand each extension's /// extender separately without reference to the full selector list, so that /// relevant results don't get trimmed too early. /// /// Returns extensions that should be added to `new_extensions` before /// extending selectors in order to properly handle extension loops such as: ///```foo /// .c {x: y; @extend .a} /// .x.y.a {@extend .b} /// .z.b {@extend .c} ///``` /// Returns `None` if there are no extensions to add. fn extend_existing_extensions( &mut self, extensions: Vec, new_extensions: &HashMap>, ) -> Option>> { let mut additional_extensions: Option< HashMap>, > = None; for extension in extensions { let mut sources = self .extensions .get(&extension.target.clone().unwrap()) .unwrap() .clone(); // `extend_existing_selectors` would have thrown already. let selectors: Vec = if let Some(v) = self.extend_complex( extension.extender.clone(), Some(new_extensions), &extension.media_context, ) { v } else { continue; }; // todo: when we add error handling, this error is special /* } on SassException catch (error) { throw SassException( "From ${extension.extenderSpan.message('')}\n" "${error.message}", error.span); } */ let contains_extension = selectors.first() == Some(&extension.extender); let mut first = false; for complex in selectors { // If the output contains the original complex selector, there's no // need to recreate it. if contains_extension && first { first = false; continue; } let with_extender = extension.clone().with_extender(complex.clone()); let existing_extension = sources.get(&complex); if let Some(existing_extension) = existing_extension.cloned() { sources.get_mut(&complex).replace( &mut MergedExtension::merge(existing_extension.clone(), with_extender) .unwrap(), ); } else { sources .get_mut(&complex) .replace(&mut with_extender.clone()); for component in complex.components.clone() { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { self.extensions_by_extender .entry(simple) .or_insert_with(Vec::new) .push(with_extender.clone()); } } } if new_extensions.contains_key(&extension.target.clone().unwrap()) { additional_extensions .get_or_insert_with(HashMap::new) .entry(extension.target.clone().unwrap()) .or_insert_with(IndexMap::new) .insert(complex.clone(), with_extender.clone()); } } } // If `selectors` doesn't contain `extension.extender`, for example if it // was replaced due to :not() expansion, we must get rid of the old // version. if !contains_extension { // todo: evaluate whether we could get away with swap_remove sources.shift_remove(&extension.extender); } } additional_extensions } /// Extend `extensions` using `new_extensions`. fn extend_existing_selectors( &mut self, selectors: SelectorHashSet, new_extensions: &HashMap>, ) { for mut selector in selectors { let old_value = selector.clone().into_selector().0; selector.set_inner(self.extend_list( old_value.clone(), Some(new_extensions), &self.media_contexts.get(&old_value).cloned(), )); /* todo: error handling } on SassException catch (error) { throw SassException( "From ${selector.span.message('')}\n" "${error.message}", error.span); } */ // If no extends actually happened (for example because unification // failed), we don't need to re-register the selector. let selector_as_selector = selector.clone().into_selector().0; if old_value == selector_as_selector { continue; } self.register_selector(selector_as_selector, &selector); } } } /// Rotates the element in list from `start` (inclusive) to `end` (exclusive) /// one index higher, looping the final element back to `start`. fn rotate_slice(list: &mut VecDeque, start: usize, end: usize) { let mut element = list.get(end - 1).unwrap().clone(); for i in start..end { let next = list.get(i).unwrap().clone(); list[i] = element; element = next; } } /// Like `HashMap::extend`, but for two-layer maps. /// /// This avoids copying inner maps from `source` if possible. fn map_add_all_2( destination: &mut HashMap>, source: HashMap>, ) { for (key, mut inner) in source { if destination.contains_key(&key) { destination .get_mut(&key) .get_or_insert(&mut IndexMap::new()) .extend(inner); } else { destination.get_mut(&key).replace(&mut inner); } } } grass-0.13.4/crates/compiler/src/selector/extend/rule.rs000066400000000000000000000001241465374720000232310ustar00rootroot00000000000000#[derive(Clone, Debug)] pub(crate) struct ExtendRule { pub is_optional: bool, } grass-0.13.4/crates/compiler/src/selector/list.rs000066400000000000000000000227241465374720000217600ustar00rootroot00000000000000use std::{ collections::VecDeque, fmt::{self, Write}, hash::{Hash, Hasher}, mem, }; use codemap::Span; use super::{unify_complex, ComplexSelector, ComplexSelectorComponent}; use crate::{ common::{Brackets, ListSeparator, QuoteKind}, error::SassResult, value::Value, }; /// A selector list. /// /// A selector list is composed of `ComplexSelector`s. It matches an element /// that matches any of the component selectors. #[derive(Clone, Debug)] pub(crate) struct SelectorList { /// The components of this selector. /// /// This is never empty. pub components: Vec, pub span: Span, } impl PartialEq for SelectorList { fn eq(&self, other: &SelectorList) -> bool { self.components == other.components } } impl Eq for SelectorList {} impl Hash for SelectorList { fn hash(&self, state: &mut H) { self.components.hash(state); } } impl fmt::Display for SelectorList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let complexes = self.components.iter().filter(|c| !c.is_invisible()); let mut first = true; for complex in complexes { if first { first = false; } else { f.write_char(',')?; if complex.line_break { f.write_char('\n')?; } else { f.write_char(' ')?; } } write!(f, "{}", complex)?; } Ok(()) } } impl SelectorList { pub fn is_invisible(&self) -> bool { self.components.iter().all(ComplexSelector::is_invisible) } pub fn contains_parent_selector(&self) -> bool { self.components .iter() .any(ComplexSelector::contains_parent_selector) } pub const fn new(span: Span) -> Self { Self { components: Vec::new(), span, } } pub fn is_empty(&self) -> bool { self.components.is_empty() } /// Returns a `SassScript` list that represents this selector. /// /// This has the same format as a list returned by `selector-parse()`. pub fn to_sass_list(self) -> Value { Value::List( self.components .into_iter() .map(|complex| { Value::List( complex .components .into_iter() .map(|complex_component| { Value::String(complex_component.to_string(), QuoteKind::None) }) .collect(), ListSeparator::Space, Brackets::None, ) }) .collect(), ListSeparator::Comma, Brackets::None, ) } /// Returns a `SelectorList` that matches only elements that are matched by /// both this and `other`. /// /// If no such list can be produced, returns `None`. pub fn unify(self, other: &Self) -> Option { let contents: Vec = self .components .into_iter() .flat_map(|c1| { other.clone().components.into_iter().flat_map(move |c2| { let unified: Option>> = unify_complex(vec![c1.components.clone(), c2.components]); if let Some(u) = unified { u.into_iter() .map(|c| ComplexSelector::new(c, false)) .collect() } else { Vec::new() } }) }) .collect(); if contents.is_empty() { return None; } Some(Self { components: contents, span: self.span.merge(other.span), }) } /// Returns a new list with all `SimpleSelector::Parent`s replaced with `parent`. /// /// If `implicit_parent` is true, this treats `ComplexSelector`s that don't /// contain an explicit `SimpleSelector::Parent` as though they began with one. /// /// The given `parent` may be `None`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit /// `SimpleSelector::Parent`s. If it does, this returns a `SassError`. pub fn resolve_parent_selectors( self, parent: Option, implicit_parent: bool, ) -> SassResult { let parent = match parent { Some(p) => p, None => { if !self.contains_parent_selector() { return Ok(self); } return Err(( "Top-level selectors may not contain the parent selector \"&\".", self.span, ) .into()); } }; Ok(Self { components: flatten_vertically( self.components .into_iter() .map(|complex| { if !complex.contains_parent_selector() { if !implicit_parent { return Ok(vec![complex]); } return Ok(parent .clone() .components .into_iter() .map(move |parent_complex| { let mut components = parent_complex.components; components.append(&mut complex.components.clone()); ComplexSelector::new( components, complex.line_break || parent_complex.line_break, ) }) .collect()); } let mut new_complexes: Vec> = vec![Vec::new()]; let mut line_breaks = vec![false]; for component in complex.components { if component.is_compound() { let resolved = match component .clone() .resolve_parent_selectors(self.span, parent.clone())? { Some(r) => r, None => { for new_complex in &mut new_complexes { new_complex.push(component.clone()); } continue; } }; let previous_complexes = mem::take(&mut new_complexes); let previous_line_breaks = mem::take(&mut line_breaks); for (i, new_complex) in previous_complexes.into_iter().enumerate() { // todo: use .get(i) let line_break = previous_line_breaks[i]; for mut resolved_complex in resolved.clone() { let mut new_this_complex = new_complex.clone(); new_this_complex.append(&mut resolved_complex.components); new_complexes.push(mem::take(&mut new_this_complex)); line_breaks.push(line_break || resolved_complex.line_break); } } } else { for new_complex in &mut new_complexes { new_complex.push(component.clone()); } } } let mut i = 0; Ok(new_complexes .into_iter() .map(|new_complex| { i += 1; ComplexSelector::new(new_complex, line_breaks[i - 1]) }) .collect()) }) .collect::>>>()?, ), span: self.span, }) } pub fn is_superselector(&self, other: &Self) -> bool { other.components.iter().all(|complex1| { self.components .iter() .any(|complex2| complex2.is_super_selector(complex1)) }) } } fn flatten_vertically(iterable: Vec>) -> Vec { let mut queues: Vec> = iterable.into_iter().map(VecDeque::from).collect(); let mut result = Vec::new(); while !queues.is_empty() { for queue in &mut queues { if queue.is_empty() { continue; } result.push(queue.pop_front().unwrap()); } queues.retain(|queue| !queue.is_empty()); } result } grass-0.13.4/crates/compiler/src/selector/mod.rs000066400000000000000000000032031465374720000215530ustar00rootroot00000000000000use codemap::Span; use crate::{error::SassResult, value::Value}; pub(crate) use attribute::Attribute; pub(crate) use common::*; pub(crate) use complex::*; pub(crate) use compound::*; pub(crate) use extend::*; pub(crate) use list::*; pub(crate) use parse::*; pub(crate) use simple::*; mod attribute; mod common; mod complex; mod compound; mod extend; mod list; mod parse; mod simple; // todo: delete this selector wrapper #[derive(Clone, Debug, Eq, PartialEq)] pub struct Selector(pub(crate) SelectorList); impl Selector { /// Small wrapper around `SelectorList`'s method that turns an empty parent selector /// into `None`. This is a hack and in the future should be replaced. // todo: take Option for parent pub fn resolve_parent_selectors( &self, parent: &Self, implicit_parent: bool, ) -> SassResult { Ok(Self(self.0.clone().resolve_parent_selectors( if parent.is_empty() { None } else { Some(parent.0.clone()) }, implicit_parent, )?)) } pub fn is_super_selector(&self, other: &Self) -> bool { self.0.is_superselector(&other.0) } pub fn contains_parent_selector(&self) -> bool { self.0.contains_parent_selector() } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub const fn new(span: Span) -> Selector { Selector(SelectorList::new(span)) } pub fn into_value(self) -> Value { self.0.to_sass_list() } pub fn unify(self, other: &Self) -> Option { Some(Selector(self.0.unify(&other.0)?)) } } grass-0.13.4/crates/compiler/src/selector/parse.rs000066400000000000000000000410131465374720000221070ustar00rootroot00000000000000use codemap::Span; use crate::{common::unvendor, error::SassResult, lexer::Lexer, parse::BaseParser, Token}; use super::{ Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo, QualifiedName, SelectorList, SimpleSelector, }; #[derive(PartialEq)] enum DevouredWhitespace { /// Some whitespace was found Whitespace, /// A newline and potentially other whitespace was found Newline, /// No whitespace was found None, } /// Pseudo-class selectors that take unadorned selectors as arguments. const SELECTOR_PSEUDO_CLASSES: [&str; 9] = [ "not", "matches", "where", "is", "current", "any", "has", "host", "host-context", ]; /// Pseudo-element selectors that take unadorned selectors as arguments. const SELECTOR_PSEUDO_ELEMENTS: [&str; 1] = ["slotted"]; pub(crate) struct SelectorParser { /// Whether this parser allows the parent selector `&`. allows_parent: bool, /// Whether this parser allows placeholder selectors beginning with `%`. allows_placeholder: bool, pub toks: Lexer, span: Span, } impl BaseParser for SelectorParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl SelectorParser { pub fn new(toks: Lexer, allows_parent: bool, allows_placeholder: bool, span: Span) -> Self { Self { toks, allows_parent, allows_placeholder, span, } } pub fn parse(mut self) -> SassResult { let tmp = self.parse_selector_list()?; if self.toks.peek().is_some() { return Err(("expected selector.", self.span).into()); } Ok(tmp) } fn parse_selector_list(&mut self) -> SassResult { let mut components = vec![self.parse_complex_selector(false)?]; self.whitespace()?; let mut line_break = false; while self.scan_char(',') { line_break = self.eat_whitespace() == DevouredWhitespace::Newline || line_break; match self.toks.peek() { Some(Token { kind: ',', .. }) => continue, Some(..) => {} None => break, } components.push(self.parse_complex_selector(line_break)?); line_break = false; } Ok(SelectorList { components, span: self.span, }) } fn eat_whitespace(&mut self) -> DevouredWhitespace { let text = self.raw_text(Self::whitespace); if text.contains('\n') { DevouredWhitespace::Newline } else if !text.is_empty() { DevouredWhitespace::Whitespace } else { DevouredWhitespace::None } } /// Consumes a complex selector. /// /// If `line_break` is `true`, that indicates that there was a line break /// before this selector. fn parse_complex_selector(&mut self, line_break: bool) -> SassResult { let mut components = Vec::new(); loop { self.whitespace()?; // todo: can we do while let Some(..) = self.toks.peek() ? match self.toks.peek() { Some(Token { kind: '+', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::NextSibling, )); } Some(Token { kind: '>', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator(Combinator::Child)); } Some(Token { kind: '~', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::FollowingSibling, )); } Some(Token { kind: '[', .. }) | Some(Token { kind: '.', .. }) | Some(Token { kind: '#', .. }) | Some(Token { kind: '%', .. }) | Some(Token { kind: ':', .. }) // todo: ampersand? | Some(Token { kind: '&', .. }) | Some(Token { kind: '*', .. }) | Some(Token { kind: '|', .. }) => { components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } Some(..) => { if !self.looking_at_identifier() { break; } components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } None => break, } } if components.is_empty() { return Err(("expected selector.", self.span).into()); } Ok(ComplexSelector::new(components, line_break)) } fn parse_compound_selector(&mut self) -> SassResult { let mut components = vec![self.parse_simple_selector(None)?]; while let Some(Token { kind, .. }) = self.toks.peek() { if !is_simple_selector_start(kind) { break; } components.push(self.parse_simple_selector(Some(false))?); } Ok(CompoundSelector { components }) } /// Consumes a simple selector. /// /// If `allows_parent` is `Some`, this will override `self.allows_parent`. If `allows_parent` /// is `None`, it will fallback to `self.allows_parent`. fn parse_simple_selector(&mut self, allows_parent: Option) -> SassResult { match self.toks.peek() { Some(Token { kind: '[', .. }) => self.parse_attribute_selector(), Some(Token { kind: '.', .. }) => self.parse_class_selector(), Some(Token { kind: '#', .. }) => self.parse_id_selector(), Some(Token { kind: '%', .. }) => { if !self.allows_placeholder { return Err(("Placeholder selectors aren't allowed here.", self.span).into()); } self.parse_placeholder_selector() } Some(Token { kind: ':', .. }) => self.parse_pseudo_selector(), Some(Token { kind: '&', .. }) => { let allows_parent = allows_parent.unwrap_or(self.allows_parent); if !allows_parent { return Err(("Parent selectors aren't allowed here.", self.span).into()); } self.parse_parent_selector() } _ => self.parse_type_or_universal_selector(), } } fn parse_attribute_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Attribute(Box::new(Attribute::from_tokens( self, )?))) } fn parse_class_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Class(self.parse_identifier(false, false)?)) } fn parse_id_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Id(self.parse_identifier(false, false)?)) } fn parse_pseudo_selector(&mut self) -> SassResult { self.toks.next(); let element = self.scan_char(':'); let name = self.parse_identifier(false, false)?; match self.toks.peek() { Some(Token { kind: '(', .. }) => self.toks.next(), _ => { return Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), name, selector: None, is_syntactic_class: !element, argument: None, span: self.span, })); } }; self.whitespace()?; let unvendored = unvendor(&name); let mut argument: Option> = None; let mut selector: Option> = None; if element { // todo: lowercase? if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.whitespace()?; } else { argument = Some(self.declaration_value(true)?.into_boxed_str()); } self.expect_char(')')?; } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.whitespace()?; self.expect_char(')')?; } else if unvendored == "nth-child" || unvendored == "nth-last-child" { let mut this_arg = self.parse_a_n_plus_b()?; self.whitespace()?; let last_was_whitespace = matches!( self.toks.peek_n_backwards(1), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) ); if last_was_whitespace && !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) { self.expect_identifier("of", false)?; this_arg.push_str(" of"); self.whitespace()?; selector = Some(Box::new(self.parse_selector_list()?)); } self.expect_char(')')?; argument = Some(this_arg.into_boxed_str()); } else { argument = Some( self.declaration_value(true)? .trim_end() .to_owned() .into_boxed_str(), ); self.expect_char(')')?; } Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), name, selector, is_syntactic_class: !element, argument, span: self.span, })) } fn parse_parent_selector(&mut self) -> SassResult { self.toks.next(); let suffix = if self.looking_at_identifier_body() { let mut buffer = String::new(); self.parse_identifier_body(&mut buffer, false, false)?; Some(buffer) } else { None }; Ok(SimpleSelector::Parent(suffix)) } fn parse_placeholder_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Placeholder( self.parse_identifier(false, false)?, )) } /// Consumes a type selector or a universal selector. /// /// These are combined because either one could start with `*`. fn parse_type_or_universal_selector(&mut self) -> SassResult { match self.toks.peek() { Some(Token { kind: '*', .. }) => { self.toks.next(); if let Some(Token { kind: '|', .. }) = self.toks.peek() { self.toks.next(); if let Some(Token { kind: '*', .. }) = self.toks.peek() { self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Asterisk)); } return Ok(SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Asterisk, })); } return Ok(SimpleSelector::Universal(Namespace::None)); } Some(Token { kind: '|', .. }) => { self.toks.next(); match self.toks.peek() { Some(Token { kind: '*', .. }) => { self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Empty)); } _ => { return Ok(SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Empty, })); } } } _ => {} } let name_or_namespace = self.parse_identifier(false, false)?; Ok(match self.toks.peek() { Some(Token { kind: '|', .. }) => { self.toks.next(); if let Some(Token { kind: '*', .. }) = self.toks.peek() { self.toks.next(); SimpleSelector::Universal(Namespace::Other(name_or_namespace.into_boxed_str())) } else { SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } } Some(..) | None => SimpleSelector::Type(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }), }) } /// Consumes an [`An+B` production][An+B] and returns its text. /// /// [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax fn parse_a_n_plus_b(&mut self) -> SassResult { let mut buf = String::new(); match self.toks.peek() { Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) => { self.expect_identifier("even", false)?; return Ok("even".to_owned()); } Some(Token { kind: 'o', .. }) | Some(Token { kind: 'O', .. }) => { self.expect_identifier("odd", false)?; return Ok("odd".to_owned()); } Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) => { buf.push(t.kind); self.toks.next(); } _ => {} } match self.toks.peek() { Some(t) if t.kind.is_ascii_digit() => { while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); self.toks.next(); } self.whitespace()?; if !self.scan_ident_char('n', false)? { return Ok(buf); } } Some(..) => self.expect_ident_char('n', false)?, None => return Err(("expected more input.", self.span).into()), } buf.push('n'); self.whitespace()?; if let Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) = self.toks.peek() { buf.push(t.kind); self.toks.next(); self.whitespace()?; match self.toks.peek() { Some(t) if !t.kind.is_ascii_digit() => { return Err(("Expected a number.", self.span).into()) } None => return Err(("Expected a number.", self.span).into()), Some(..) => {} } while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); self.toks.next(); } } Ok(buf) } } /// Returns whether `c` can start a simple selector other than a type /// selector. fn is_simple_selector_start(c: char) -> bool { matches!(c, '*' | '[' | '.' | '#' | '%' | ':') } /// Returns whether `name` is the name of a pseudo-element that can be written /// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or /// `:first-letter`) fn is_fake_pseudo_element(name: &str) -> bool { match name.as_bytes().first() { Some(b'a') | Some(b'A') => name.to_ascii_lowercase() == "after", Some(b'b') | Some(b'B') => name.to_ascii_lowercase() == "before", Some(b'f') | Some(b'F') => matches!( name.to_ascii_lowercase().as_str(), "first-line" | "first-letter" ), _ => false, } } grass-0.13.4/crates/compiler/src/selector/simple.rs000066400000000000000000000536661465374720000223070ustar00rootroot00000000000000use std::{ fmt::{self, Write}, hash::{Hash, Hasher}, }; use codemap::Span; use crate::{common::unvendor, error::SassResult}; use super::{ Attribute, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, QualifiedName, SelectorList, Specificity, }; const SUBSELECTOR_PSEUDOS: [&str; 6] = [ "matches", "where", "is", "any", "nth-child", "nth-last-child", ]; const BASE_SPECIFICITY: i32 = 1000; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum SimpleSelector { /// * Universal(Namespace), /// A pseudo-class or pseudo-element selector. /// /// The semantics of a specific pseudo selector depends on its name. Some /// selectors take arguments, including other selectors. Sass manually encodes /// logic for each pseudo selector that takes a selector as an argument, to /// ensure that extension and other selector operations work properly. Pseudo(Pseudo), /// A type selector. /// /// This selects elements whose name equals the given name. Type(QualifiedName), /// A placeholder selector. /// /// This doesn't match any elements. It's intended to be extended using /// `@extend`. It's not a plain CSS selector—it should be removed before /// emitting a CSS document. Placeholder(String), /// A selector that matches the parent in the Sass stylesheet. /// `&` /// /// This is not a plain CSS selector—it should be removed before emitting a CSS /// document. /// /// The parameter is the suffix that will be added to the parent selector after /// it's been resolved. /// /// This is assumed to be a valid identifier suffix. It may be `None`, /// indicating that the parent selector will not be modified. Parent(Option), Id(String), /// A class selector. /// /// This selects elements whose `class` attribute contains an identifier with /// the given name. Class(String), Attribute(Box), } impl fmt::Display for SimpleSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Id(name) => write!(f, "#{}", name), Self::Class(name) => write!(f, ".{}", name), Self::Placeholder(name) => write!(f, "%{}", name), Self::Universal(namespace) => write!(f, "{}*", namespace), Self::Pseudo(pseudo) => write!(f, "{}", pseudo), Self::Type(name) => write!(f, "{}", name), Self::Attribute(attr) => write!(f, "{}", attr), Self::Parent(..) => unreachable!("It should not be possible to format `&`."), } } } impl SimpleSelector { /// The minimum possible specificity that this selector can have. /// /// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// can have a range of possible specificities. /// /// Specifity is represented in base 1000. The spec says this should be /// "sufficiently high"; it's extremely unlikely that any single selector /// sequence will contain 1000 simple selectors. pub fn min_specificity(&self) -> i32 { match self { Self::Universal(..) => 0, Self::Type(..) => 1, Self::Pseudo(pseudo) => pseudo.min_specificity(), Self::Id(..) => BASE_SPECIFICITY.pow(2_u32), _ => BASE_SPECIFICITY, } } /// The maximum possible specificity that this selector can have. /// /// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// can have a range of possible specificities. pub fn max_specificity(&self) -> i32 { match self { Self::Universal(..) => 0, Self::Pseudo(pseudo) => pseudo.max_specificity(), _ => self.min_specificity(), } } pub fn is_invisible(&self) -> bool { match self { Self::Universal(..) | Self::Type(..) | Self::Id(..) | Self::Class(..) | Self::Attribute(..) => false, Self::Pseudo(Pseudo { name, selector, .. }) => { name != "not" && selector.as_ref().map_or(false, |sel| sel.is_invisible()) } Self::Placeholder(..) => true, Self::Parent(..) => unreachable!("parent selectors should be resolved at this point"), } } pub fn add_suffix(&mut self, suffix: &str, span: Span) -> SassResult<()> { match self { Self::Type(name) => name.ident.push_str(suffix), Self::Placeholder(name) | Self::Id(name) | Self::Class(name) | Self::Pseudo(Pseudo { name, argument: None, selector: None, .. }) => name.push_str(suffix), // todo: add test for this? _ => return Err((format!("Invalid parent selector \"{}\"", self), span).into()), }; Ok(()) } pub fn is_universal(&self) -> bool { matches!(self, Self::Universal(..)) } pub fn is_pseudo(&self) -> bool { matches!(self, Self::Pseudo { .. }) } pub fn is_parent(&self) -> bool { matches!(self, Self::Parent(..)) } pub fn is_id(&self) -> bool { matches!(self, Self::Id(..)) } pub fn is_type(&self) -> bool { matches!(self, Self::Type(..)) } pub fn unify(self, compound: Vec) -> Option> { match self { Self::Type(..) => self.unify_type(compound), Self::Universal(..) => self.unify_universal(compound), Self::Pseudo { .. } => self.unify_pseudo(compound), Self::Id(..) => { if compound .iter() .any(|simple| simple.is_id() && simple != &self) { return None; } self.unify_default(compound) } _ => self.unify_default(compound), } } /// Returns the components of a `CompoundSelector` that matches only elements /// matched by both this and `compound`. /// /// By default, this just returns a copy of `compound` with this selector /// added to the end, or returns the original array if this selector already /// exists in it. /// /// Returns `None` if unification is impossible—for example, if there are /// multiple ID selectors. fn unify_default(self, mut compound: Vec) -> Option> { if compound.len() == 1 && compound[0].is_universal() { return compound.swap_remove(0).unify(vec![self]); } if compound.contains(&self) { return Some(compound); } let mut result: Vec = Vec::new(); let mut added_this = false; for simple in compound { if !added_this && simple.is_pseudo() { result.push(self.clone()); added_this = true; } result.push(simple); } if !added_this { result.push(self); } Some(result) } fn unify_universal(self, mut compound: Vec) -> Option> { if let Self::Universal(..) | Self::Type(..) = compound[0] { let mut unified = vec![self.unify_universal_and_element(&compound[0])?]; unified.extend(compound.into_iter().skip(1)); return Some(unified); } if self != Self::Universal(Namespace::Asterisk) && self != Self::Universal(Namespace::None) { let mut v = vec![self]; v.append(&mut compound); return Some(v); } if !compound.is_empty() { return Some(compound); } Some(vec![self]) } /// Returns a `SimpleSelector` that matches only elements that are matched by /// both `selector1` and `selector2`, which must both be either /// `SimpleSelector::Universal`s or `SimpleSelector::Type`s. /// /// If no such selector can be produced, returns `None`. fn unify_universal_and_element(&self, other: &Self) -> Option { let namespace1; let name1; if let SimpleSelector::Type(name) = self.clone() { namespace1 = name.namespace; name1 = name.ident; } else if let SimpleSelector::Universal(namespace) = self.clone() { namespace1 = namespace; name1 = String::new(); } else { unreachable!("{:?} must be a universal selector or a type selector", self); } let namespace2; let mut name2 = String::new(); if let SimpleSelector::Universal(namespace) = other { namespace2 = namespace.clone(); } else if let SimpleSelector::Type(name) = other { namespace2 = name.namespace.clone(); name2 = name.ident.clone(); } else { unreachable!( "{:?} must be a universal selector or a type selector", other ); } let namespace = if namespace1 == namespace2 || namespace2 == Namespace::Asterisk { namespace1 } else if namespace1 == Namespace::Asterisk { namespace2 } else { return None; }; let name = if name1 == name2 || name2.is_empty() { name1 } else if name1.is_empty() || name1 == "*" { name2 } else { return None; }; Some(if name.is_empty() { SimpleSelector::Universal(namespace) } else { SimpleSelector::Type(QualifiedName { namespace, ident: name, }) }) } fn unify_type(self, mut compound: Vec) -> Option> { if let Self::Universal(..) | Self::Type(..) = compound[0] { let mut unified = vec![self.unify_universal_and_element(&compound[0])?]; unified.extend(compound.into_iter().skip(1)); Some(unified) } else { let mut unified = vec![self]; unified.append(&mut compound); Some(unified) } } fn unify_pseudo(self, mut compound: Vec) -> Option> { if compound.len() == 1 && compound[0].is_universal() { return compound.remove(0).unify(vec![self]); } if compound.contains(&self) { return Some(compound); } let mut result = Vec::new(); let mut added_self = false; for simple in compound { if let Self::Pseudo(Pseudo { is_class: false, .. }) = simple { // A given compound selector may only contain one pseudo element. If // `compound` has a different one than `self`, unification fails. if let Self::Pseudo(Pseudo { is_class: false, .. }) = self { return None; } // Otherwise, this is a pseudo selector and should come before pseduo // elements. result.push(self.clone()); added_self = true; } result.push(simple); } if !added_self { result.push(self); } Some(result) } pub fn is_super_selector_of_compound(&self, compound: &CompoundSelector) -> bool { compound.components.iter().any(|their_simple| { if self == their_simple { return true; } if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), name, .. }) = their_simple { if SUBSELECTOR_PSEUDOS.contains(&unvendor(name)) { return sel.components.iter().all(|complex| { if complex.components.len() != 1 { return false; }; complex .components .first() .unwrap() .as_compound() .components .contains(self) }); } false } else { false } }) } } #[derive(Clone, Debug)] pub(crate) struct Pseudo { /// The name of this selector. pub name: String, /// Whether this is a pseudo-class selector. /// /// If this is false, this is a pseudo-element selector pub is_class: bool, /// Whether this is syntactically a pseudo-class selector. /// /// This is the same as `is_class` unless this selector is a pseudo-element /// that was written syntactically as a pseudo-class (`:before`, `:after`, /// `:first-line`, or `:first-letter`). /// /// If this is false, it is syntactically a psuedo-element pub is_syntactic_class: bool, /// The non-selector argument passed to this selector. /// /// This is `None` if there's no argument. If `argument` and `selector` are /// both non-`None`, the selector follows the argument. pub argument: Option>, /// The selector argument passed to this selector. /// /// This is `None` if there's no selector. If `argument` and `selector` are /// both non-`None`, the selector follows the argument. pub selector: Option>, pub span: Span, } impl PartialEq for Pseudo { fn eq(&self, other: &Pseudo) -> bool { self.name == other.name && self.is_class == other.is_class && self.argument == other.argument && self.selector == other.selector } } impl Eq for Pseudo {} impl Hash for Pseudo { fn hash(&self, state: &mut H) { self.name.hash(state); self.is_class.hash(state); self.argument.hash(state); self.selector.hash(state); } } impl fmt::Display for Pseudo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(sel) = &self.selector { if self.name == "not" && sel.is_invisible() { return Ok(()); } } f.write_char(':')?; if !self.is_syntactic_class { f.write_char(':')?; } f.write_str(&self.name)?; if self.argument.is_none() && self.selector.is_none() { return Ok(()); } f.write_char('(')?; if let Some(arg) = &self.argument { f.write_str(arg)?; if self.selector.is_some() { f.write_char(' ')?; } } if let Some(sel) = &self.selector { write!(f, "{}", sel)?; } f.write_char(')') } } impl Pseudo { /// Returns whether `pseudo1` is a superselector of `compound2`. /// /// That is, whether `pseudo1` matches every element that `compound2` matches, as well /// as possibly additional elements. /// /// This assumes that `pseudo1`'s `selector` argument is not `None`. /// /// If `parents` is passed, it represents the parents of `compound`. This is /// relevant for pseudo selectors with selector arguments, where we may need to /// know if the parent selectors in the selector argument match `parents`. pub fn is_super_selector( &self, compound: &CompoundSelector, parents: Option>, ) -> bool { debug_assert!(self.selector.is_some()); match self.normalized_name() { "matches" | "is" | "any" | "where" => { selector_pseudos_named(compound.clone(), &self.name, true).any(move |pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(&pseudo2.selector.unwrap()) }) || self .selector .as_ref() .unwrap() .components .iter() .any(move |complex1| { let mut components = parents.clone().unwrap_or_default(); components.push(ComplexSelectorComponent::Compound(compound.clone())); complex1.is_super_selector(&ComplexSelector::new(components, false)) }) } "has" | "host" | "host-context" => { selector_pseudos_named(compound.clone(), &self.name, true).any(|pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(&pseudo2.selector.unwrap()) }) } "slotted" => { selector_pseudos_named(compound.clone(), &self.name, false).any(|pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(pseudo2.selector.as_ref().unwrap()) }) } "not" => self .selector .as_ref() .unwrap() .components .iter() .all(|complex| { compound.components.iter().any(|simple2| { if let SimpleSelector::Type(..) = simple2 { let compound1 = complex.components.last(); if let Some(ComplexSelectorComponent::Compound(c)) = compound1 { c.components .iter() .any(|simple1| simple1.is_type() && simple1 != simple2) } else { false } } else if let SimpleSelector::Id(..) = simple2 { let compound1 = complex.components.last(); if let Some(ComplexSelectorComponent::Compound(c)) = compound1 { c.components .iter() .any(|simple1| simple1.is_id() && simple1 != simple2) } else { false } } else if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), name, .. }) = simple2 { if name != &self.name { return false; } sel.is_superselector(&SelectorList { components: vec![complex.clone()], span: self.span, }) } else { false } }) }), "current" => selector_pseudos_named(compound.clone(), &self.name, self.is_class) .any(|pseudo2| self.selector == pseudo2.selector), "nth-child" | "nth-last-child" => compound.components.iter().any(|pseudo2| { if let SimpleSelector::Pseudo( pseudo @ Pseudo { selector: Some(..), .. }, ) = pseudo2 { pseudo.name == self.name && pseudo.argument == self.argument && self .selector .as_ref() .unwrap() .is_superselector(pseudo.selector.as_ref().unwrap()) } else { false } }), _ => unreachable!(), } } #[allow(clippy::missing_const_for_fn)] pub fn with_selector(self, selector: Option>) -> Self { Self { selector, ..self } } pub fn max_specificity(&self) -> i32 { self.specificity().max } pub fn min_specificity(&self) -> i32 { self.specificity().min } pub fn specificity(&self) -> Specificity { if !self.is_class { return Specificity { min: 1, max: 1 }; } let selector = match &self.selector { Some(sel) => sel, None => { return Specificity { min: BASE_SPECIFICITY, max: BASE_SPECIFICITY, } } }; if self.name == "not" { let mut min = 0; let mut max = 0; for complex in &selector.components { min = min.max(complex.min_specificity()); max = max.max(complex.max_specificity()); } Specificity { min, max } } else { // This is higher than any selector's specificity can actually be. let mut min = BASE_SPECIFICITY.pow(3_u32); let mut max = 0; for complex in &selector.components { min = min.min(complex.min_specificity()); max = max.max(complex.max_specificity()); } Specificity { min, max } } } /// Like `name`, but without any vendor prefixes. pub fn normalized_name(&self) -> &str { unvendor(&self.name) } } /// Returns all pseudo selectors in `compound` that have a selector argument, /// and that have the given `name`. fn selector_pseudos_named( compound: CompoundSelector, name: &str, is_class: bool, ) -> impl Iterator + '_ { compound .components .into_iter() .filter_map(|c| { if let SimpleSelector::Pseudo(p) = c { Some(p) } else { None } }) .filter(move |p| p.is_class == is_class && p.selector.is_some() && p.name == name) } grass-0.13.4/crates/compiler/src/serializer.rs000066400000000000000000001113461465374720000213350ustar00rootroot00000000000000use std::io::Write; use codemap::{CodeMap, Span}; use crate::{ ast::{CssStmt, MediaQuery, Style, SupportsRule}, color::{Color, ColorFormat, NAMED_COLORS}, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, error::SassResult, selector::{ Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo, SelectorList, SimpleSelector, }, utils::hex_char_for, value::{ fuzzy_equals, ArgList, CalculationArg, CalculationName, SassCalculation, SassFunction, SassMap, SassNumber, Value, }, Options, }; pub(crate) fn serialize_selector_list( list: &SelectorList, options: &Options, span: Span, ) -> String { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.write_selector_list(list); serializer.finish_for_expr() } pub(crate) fn serialize_calculation_arg( arg: &CalculationArg, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.write_calculation_arg(arg)?; Ok(serializer.finish_for_expr()) } pub(crate) fn serialize_number( number: &SassNumber, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.visit_number(number)?; Ok(serializer.finish_for_expr()) } pub(crate) fn serialize_value(val: &Value, options: &Options, span: Span) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.visit_value(val, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_value(val: &Value, options: &Options, span: Span) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.visit_value(val, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> String { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.write_float(number); serializer.finish_for_expr() } pub(crate) fn inspect_map(map: &SassMap, options: &Options, span: Span) -> SassResult { let code_map = CodeMap::new(); let mut serializer = Serializer::new(options, &code_map, true, span); serializer.visit_map(map, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_function_ref( func: &SassFunction, options: &Options, span: Span, ) -> SassResult { let code_map = CodeMap::new(); let mut serializer = Serializer::new(options, &code_map, true, span); serializer.visit_function_ref(func, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_number( number: &SassNumber, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.visit_number(number)?; Ok(serializer.finish_for_expr()) } pub(crate) struct Serializer<'a> { indentation: usize, options: &'a Options<'a>, inspect: bool, indent_width: usize, // todo: use this field _quote: bool, buffer: Vec, map: &'a CodeMap, span: Span, } impl<'a> Serializer<'a> { pub fn new(options: &'a Options<'a>, map: &'a CodeMap, inspect: bool, span: Span) -> Self { Self { inspect, _quote: true, indentation: 0, indent_width: 2, options, buffer: Vec::new(), map, span, } } fn omit_spaces_around_complex_component(&self, component: &ComplexSelectorComponent) -> bool { self.options.is_compressed() && matches!(component, ComplexSelectorComponent::Combinator(..)) } fn write_pseudo_selector(&mut self, pseudo: &Pseudo) { if let Some(sel) = &pseudo.selector { if pseudo.name == "not" && sel.is_invisible() { return; } } self.buffer.push(b':'); if !pseudo.is_syntactic_class { self.buffer.push(b':'); } self.buffer.extend_from_slice(pseudo.name.as_bytes()); if pseudo.argument.is_none() && pseudo.selector.is_none() { return; } self.buffer.push(b'('); if let Some(arg) = &pseudo.argument { self.buffer.extend_from_slice(arg.as_bytes()); if pseudo.selector.is_some() { self.buffer.push(b' '); } } if let Some(sel) = &pseudo.selector { self.write_selector_list(sel); } self.buffer.push(b')'); } fn write_namespace(&mut self, namespace: &Namespace) { match namespace { Namespace::Empty => self.buffer.push(b'|'), Namespace::Asterisk => self.buffer.extend_from_slice(b"*|"), Namespace::Other(namespace) => { self.buffer.extend_from_slice(namespace.as_bytes()); self.buffer.push(b'|'); } Namespace::None => {} } } fn write_simple_selector(&mut self, simple: &SimpleSelector) { match simple { SimpleSelector::Id(name) => { self.buffer.push(b'#'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Class(name) => { self.buffer.push(b'.'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Placeholder(name) => { self.buffer.push(b'%'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Universal(namespace) => { self.write_namespace(namespace); self.buffer.push(b'*'); } SimpleSelector::Pseudo(pseudo) => self.write_pseudo_selector(pseudo), SimpleSelector::Type(name) => { self.write_namespace(&name.namespace); self.buffer.extend_from_slice(name.ident.as_bytes()); } SimpleSelector::Attribute(attr) => write!(&mut self.buffer, "{}", attr).unwrap(), SimpleSelector::Parent(..) => unreachable!("It should not be possible to format `&`."), } } fn write_compound_selector(&mut self, compound: &CompoundSelector) { let mut did_write = false; for simple in &compound.components { if did_write { self.write_simple_selector(simple); } else { let len = self.buffer.len(); self.write_simple_selector(simple); if self.buffer.len() != len { did_write = true; } } } // If we emit an empty compound, it's because all of the components got // optimized out because they match all selectors, so we just emit the // universal selector. if !did_write { self.buffer.push(b'*'); } } fn write_complex_selector_component(&mut self, component: &ComplexSelectorComponent) { match component { ComplexSelectorComponent::Combinator(Combinator::NextSibling) => self.buffer.push(b'+'), ComplexSelectorComponent::Combinator(Combinator::Child) => self.buffer.push(b'>'), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling) => { self.buffer.push(b'~') } ComplexSelectorComponent::Compound(compound) => self.write_compound_selector(compound), } } fn write_complex_selector(&mut self, complex: &ComplexSelector) { let mut last_component = None; for component in &complex.components { if let Some(c) = last_component { if !self.omit_spaces_around_complex_component(c) && !self.omit_spaces_around_complex_component(component) { self.buffer.push(b' '); } } self.write_complex_selector_component(component); last_component = Some(component); } } fn write_selector_list(&mut self, list: &SelectorList) { let complexes = list.components.iter().filter(|c| !c.is_invisible()); let mut first = true; for complex in complexes { if first { first = false; } else { self.buffer.push(b','); if complex.line_break { self.write_newline(); } else { self.write_optional_space(); } } self.write_complex_selector(complex); } } fn write_newline(&mut self) { if !self.options.is_compressed() { self.buffer.push(b'\n'); } } fn write_comma_separator(&mut self) { self.buffer.push(b','); self.write_optional_space(); } fn write_calculation_name(&mut self, name: CalculationName) { match name { CalculationName::Calc => self.buffer.extend_from_slice(b"calc"), CalculationName::Min => self.buffer.extend_from_slice(b"min"), CalculationName::Max => self.buffer.extend_from_slice(b"max"), CalculationName::Clamp => self.buffer.extend_from_slice(b"clamp"), } } fn visit_calculation(&mut self, calculation: &SassCalculation) -> SassResult<()> { self.write_calculation_name(calculation.name); self.buffer.push(b'('); if let Some((last, slice)) = calculation.args.split_last() { for arg in slice { self.write_calculation_arg(arg)?; self.write_comma_separator(); } self.write_calculation_arg(last)?; } self.buffer.push(b')'); Ok(()) } fn write_calculation_arg(&mut self, arg: &CalculationArg) -> SassResult<()> { match arg { CalculationArg::Number(num) => self.visit_number(num)?, CalculationArg::Calculation(calc) => { self.visit_calculation(calc)?; } CalculationArg::String(s) | CalculationArg::Interpolation(s) => { self.buffer.extend_from_slice(s.as_bytes()); } CalculationArg::Operation { lhs, op, rhs } => { let paren_left = match &**lhs { CalculationArg::Interpolation(..) => true, CalculationArg::Operation { op: op2, .. } => op2.precedence() < op.precedence(), _ => false, }; if paren_left { self.buffer.push(b'('); } self.write_calculation_arg(lhs)?; if paren_left { self.buffer.push(b')'); } let operator_whitespace = !self.options.is_compressed() || matches!(op, BinaryOp::Plus | BinaryOp::Minus); if operator_whitespace { self.buffer.push(b' '); } // todo: avoid allocation with `write_binary_operator` method self.buffer.extend_from_slice(op.to_string().as_bytes()); if operator_whitespace { self.buffer.push(b' '); } let paren_right = match &**rhs { CalculationArg::Interpolation(..) => true, CalculationArg::Operation { op: op2, .. } => { CalculationArg::parenthesize_calculation_rhs(*op, *op2) } _ => false, }; if paren_right { self.buffer.push(b'('); } self.write_calculation_arg(rhs)?; if paren_right { self.buffer.push(b')'); } } } Ok(()) } fn write_rgb(&mut self, color: &Color) { let is_opaque = fuzzy_equals(color.alpha().0, 1.0); if is_opaque { self.buffer.extend_from_slice(b"rgb("); } else { self.buffer.extend_from_slice(b"rgba("); } self.write_float(color.red().0); self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.green().0); self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.blue().0); if !is_opaque { self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.alpha().0); } self.buffer.push(b')'); } fn write_hsl(&mut self, color: &Color) { let is_opaque = fuzzy_equals(color.alpha().0, 1.0); if is_opaque { self.buffer.extend_from_slice(b"hsl("); } else { self.buffer.extend_from_slice(b"hsla("); } self.write_float(color.hue().0); self.buffer.extend_from_slice(b"deg, "); self.write_float(color.saturation().0); self.buffer.extend_from_slice(b"%, "); self.write_float(color.lightness().0); self.buffer.extend_from_slice(b"%"); if !is_opaque { self.buffer.extend_from_slice(b", "); self.write_float(color.alpha().0); } self.buffer.push(b')'); } fn write_hex_component(&mut self, channel: u32) { debug_assert!(channel < 256); self.buffer.push(hex_char_for(channel >> 4) as u8); self.buffer.push(hex_char_for(channel & 0xF) as u8); } fn is_symmetrical_hex(channel: u32) -> bool { channel & 0xF == channel >> 4 } fn can_use_short_hex(color: &Color) -> bool { Self::is_symmetrical_hex(color.red().0.round() as u32) && Self::is_symmetrical_hex(color.green().0.round() as u32) && Self::is_symmetrical_hex(color.blue().0.round() as u32) } pub fn visit_color(&mut self, color: &Color) { let red = color.red().0.round() as u8; let green = color.green().0.round() as u8; let blue = color.blue().0.round() as u8; let name = if fuzzy_equals(color.alpha().0, 1.0) { NAMED_COLORS.get_by_rgba([red, green, blue]) } else { None }; #[allow(clippy::unnecessary_unwrap)] if self.options.is_compressed() { if fuzzy_equals(color.alpha().0, 1.0) { let hex_length = if Self::can_use_short_hex(color) { 4 } else { 7 }; if name.is_some() && name.unwrap().len() <= hex_length { self.buffer.extend_from_slice(name.unwrap().as_bytes()); } else if Self::can_use_short_hex(color) { self.buffer.push(b'#'); self.buffer.push(hex_char_for(red as u32 & 0xF) as u8); self.buffer.push(hex_char_for(green as u32 & 0xF) as u8); self.buffer.push(hex_char_for(blue as u32 & 0xF) as u8); } else { self.buffer.push(b'#'); self.write_hex_component(red as u32); self.write_hex_component(green as u32); self.write_hex_component(blue as u32); } } else { self.write_rgb(color); } } else if color.format != ColorFormat::Infer { match &color.format { ColorFormat::Rgb => self.write_rgb(color), ColorFormat::Hsl => self.write_hsl(color), ColorFormat::Literal(text) => self.buffer.extend_from_slice(text.as_bytes()), ColorFormat::Infer => unreachable!(), } // Always emit generated transparent colors in rgba format. This works // around an IE bug. See sass/sass#1782. } else if name.is_some() && !fuzzy_equals(color.alpha().0, 0.0) { self.buffer.extend_from_slice(name.unwrap().as_bytes()); } else if fuzzy_equals(color.alpha().0, 1.0) { self.buffer.push(b'#'); self.write_hex_component(red as u32); self.write_hex_component(green as u32); self.write_hex_component(blue as u32); } else { self.write_rgb(color); } } fn write_media_query(&mut self, query: &MediaQuery) { if let Some(modifier) = &query.modifier { self.buffer.extend_from_slice(modifier.as_bytes()); self.buffer.push(b' '); } if let Some(media_type) = &query.media_type { self.buffer.extend_from_slice(media_type.as_bytes()); if !query.conditions.is_empty() { self.buffer.extend_from_slice(b" and "); } } if query.conditions.len() == 1 && query.conditions.first().unwrap().starts_with("(not ") { self.buffer.extend_from_slice(b"not "); let condition = query.conditions.first().unwrap(); self.buffer .extend_from_slice(condition["(not ".len()..condition.len() - 1].as_bytes()); } else { let operator = if query.conjunction { " and " } else { " or " }; self.buffer .extend_from_slice(query.conditions.join(operator).as_bytes()); } } pub fn visit_number(&mut self, number: &SassNumber) -> SassResult<()> { if let Some(as_slash) = &number.as_slash { self.visit_number(&as_slash.0)?; self.buffer.push(b'/'); self.visit_number(&as_slash.1)?; return Ok(()); } if !self.inspect && number.unit.is_complex() { return Err(( format!( "{} isn't a valid CSS value.", inspect_number(number, self.options, self.span)? ), self.span, ) .into()); } self.write_float(number.num.0); write!(&mut self.buffer, "{}", number.unit)?; Ok(()) } fn write_float(&mut self, float: f64) { if float.is_infinite() && float.is_sign_negative() { self.buffer.extend_from_slice(b"-Infinity"); return; } else if float.is_infinite() { self.buffer.extend_from_slice(b"Infinity"); return; } // todo: can optimize away intermediate buffer let mut buffer = String::with_capacity(3); if float < 0.0 { buffer.push('-'); } let num = float.abs(); if self.options.is_compressed() && num < 1.0 { buffer.push_str( format!("{:.10}", num)[1..] .trim_end_matches('0') .trim_end_matches('.'), ); } else { buffer.push_str( format!("{:.10}", num) .trim_end_matches('0') .trim_end_matches('.'), ); } if buffer.is_empty() || buffer == "-" || buffer == "-0" { buffer = "0".to_owned(); } self.buffer.append(&mut buffer.into_bytes()); } pub fn visit_group( &mut self, stmt: CssStmt, prev_was_group_end: bool, prev_requires_semicolon: bool, ) -> SassResult<()> { if prev_requires_semicolon { self.buffer.push(b';'); } if !self.buffer.is_empty() { self.write_optional_newline(); } if prev_was_group_end && !self.buffer.is_empty() { self.write_optional_newline(); } self.visit_stmt(stmt)?; Ok(()) } fn finish_for_expr(self) -> String { // SAFETY: todo unsafe { String::from_utf8_unchecked(self.buffer) } } pub fn finish(mut self, prev_requires_semicolon: bool) -> String { let is_not_ascii = self.buffer.iter().any(|&c| !c.is_ascii()); if prev_requires_semicolon { self.buffer.push(b';'); } if !self.buffer.is_empty() { self.write_optional_newline(); } // SAFETY: todo let mut as_string = unsafe { String::from_utf8_unchecked(self.buffer) }; if is_not_ascii && self.options.is_compressed() && self.options.allows_charset { as_string.insert(0, '\u{FEFF}'); } else if is_not_ascii && self.options.allows_charset { as_string.insert_str(0, "@charset \"UTF-8\";\n"); } as_string } fn write_indentation(&mut self) { if self.options.is_compressed() { return; } self.buffer.reserve(self.indentation); for _ in 0..self.indentation { self.buffer.push(b' '); } } fn write_list_separator(&mut self, sep: ListSeparator) { match (sep, self.options.is_compressed()) { (ListSeparator::Space | ListSeparator::Undecided, _) => self.buffer.push(b' '), (ListSeparator::Comma, true) => self.buffer.push(b','), (ListSeparator::Comma, false) => self.buffer.extend_from_slice(b", "), (ListSeparator::Slash, true) => self.buffer.push(b'/'), (ListSeparator::Slash, false) => self.buffer.extend_from_slice(b" / "), } } fn elem_needs_parens(sep: ListSeparator, elem: &Value) -> bool { match elem { Value::List(elems, sep2, brackets) => { if elems.len() < 2 { return false; } if *brackets == Brackets::Bracketed { return false; } match sep { ListSeparator::Comma => *sep2 == ListSeparator::Comma, ListSeparator::Slash => { *sep2 == ListSeparator::Comma || *sep2 == ListSeparator::Slash } _ => *sep2 != ListSeparator::Undecided, } } _ => false, } } fn visit_list( &mut self, list_elems: &[Value], sep: ListSeparator, brackets: Brackets, span: Span, ) -> SassResult<()> { if brackets == Brackets::Bracketed { self.buffer.push(b'['); } else if list_elems.is_empty() { if !self.inspect { return Err(("() isn't a valid CSS value.", span).into()); } self.buffer.extend_from_slice(b"()"); return Ok(()); } let is_singleton = self.inspect && list_elems.len() == 1 && (sep == ListSeparator::Comma || sep == ListSeparator::Slash); if is_singleton && brackets != Brackets::Bracketed { self.buffer.push(b'('); } let (mut x, mut y); let elems: &mut dyn Iterator = if self.inspect { x = list_elems.iter(); &mut x } else { y = list_elems.iter().filter(|elem| !elem.is_blank()); &mut y }; let mut elems = elems.peekable(); while let Some(elem) = elems.next() { if self.inspect { let needs_parens = Self::elem_needs_parens(sep, elem); if needs_parens { self.buffer.push(b'('); } self.visit_value(elem, span)?; if needs_parens { self.buffer.push(b')'); } } else { self.visit_value(elem, span)?; } if elems.peek().is_some() { self.write_list_separator(sep); } } if is_singleton { match sep { ListSeparator::Comma => self.buffer.push(b','), ListSeparator::Slash => self.buffer.push(b'/'), _ => unreachable!(), } if brackets != Brackets::Bracketed { self.buffer.push(b')'); } } if brackets == Brackets::Bracketed { self.buffer.push(b']'); } Ok(()) } fn write_map_element(&mut self, value: &Value, span: Span) -> SassResult<()> { let needs_parens = matches!(value, Value::List(_, ListSeparator::Comma, Brackets::None)); if needs_parens { self.buffer.push(b'('); } self.visit_value(value, span)?; if needs_parens { self.buffer.push(b')'); } Ok(()) } fn visit_map(&mut self, map: &SassMap, span: Span) -> SassResult<()> { if !self.inspect { return Err(( format!( "{} isn't a valid CSS value.", inspect_map(map, self.options, span)? ), span, ) .into()); } self.buffer.push(b'('); let mut elems = map.iter().peekable(); while let Some((k, v)) = elems.next() { self.write_map_element(&k.node, k.span)?; self.buffer.extend_from_slice(b": "); self.write_map_element(v, k.span)?; if elems.peek().is_some() { self.buffer.extend_from_slice(b", "); } } self.buffer.push(b')'); Ok(()) } fn visit_unquoted_string(&mut self, string: &str) { let mut after_newline = false; self.buffer.reserve(string.len()); for c in string.bytes() { match c { b'\n' => { self.buffer.push(b' '); after_newline = true; } b' ' => { if !after_newline { self.buffer.push(b' '); } } _ => { self.buffer.push(c); after_newline = false; } } } } fn visit_quoted_string(&mut self, force_double_quote: bool, string: &str) { let mut has_single_quote = false; let mut has_double_quote = false; let mut buffer = Vec::new(); if force_double_quote { buffer.push(b'"'); } let mut iter = string.as_bytes().iter().copied().peekable(); while let Some(c) = iter.next() { match c { b'\'' => { if force_double_quote { buffer.push(b'\''); } else if has_double_quote { self.visit_quoted_string(true, string); return; } else { has_single_quote = true; buffer.push(b'\''); } } b'"' => { if force_double_quote { buffer.push(b'\\'); buffer.push(b'"'); } else if has_single_quote { self.visit_quoted_string(true, string); return; } else { has_double_quote = true; buffer.push(b'"'); } } b'\x00'..=b'\x08' | b'\x0A'..=b'\x1F' => { buffer.push(b'\\'); if c as u32 > 0xF { buffer.push(hex_char_for(c as u32 >> 4) as u8); } buffer.push(hex_char_for(c as u32 & 0xF) as u8); let next = match iter.peek() { Some(v) => *v, None => break, }; if next.is_ascii_hexdigit() || next == b' ' || next == b'\t' { buffer.push(b' '); } } b'\\' => { buffer.push(b'\\'); buffer.push(b'\\'); } _ => buffer.push(c), } } if force_double_quote { buffer.push(b'"'); self.buffer.extend_from_slice(&buffer); } else { let quote = if has_double_quote { b'\'' } else { b'"' }; self.buffer.push(quote); self.buffer.extend_from_slice(&buffer); self.buffer.push(quote); } } fn visit_function_ref(&mut self, func: &SassFunction, span: Span) -> SassResult<()> { if !self.inspect { return Err(( format!( "{} isn't a valid CSS value.", inspect_function_ref(func, self.options, span)? ), span, ) .into()); } self.buffer.extend_from_slice(b"get-function("); self.visit_quoted_string(false, func.name().as_str()); self.buffer.push(b')'); Ok(()) } fn visit_arglist(&mut self, arglist: &ArgList, span: Span) -> SassResult<()> { self.visit_list(&arglist.elems, ListSeparator::Comma, Brackets::None, span) } fn visit_value(&mut self, value: &Value, span: Span) -> SassResult<()> { match value { Value::Dimension(num) => self.visit_number(num)?, Value::Color(color) => self.visit_color(color), Value::Calculation(calc) => self.visit_calculation(calc)?, Value::List(elems, sep, brackets) => self.visit_list(elems, *sep, *brackets, span)?, Value::True => self.buffer.extend_from_slice(b"true"), Value::False => self.buffer.extend_from_slice(b"false"), Value::Null => { if self.inspect { self.buffer.extend_from_slice(b"null") } } Value::Map(map) => self.visit_map(map, span)?, Value::FunctionRef(func) => self.visit_function_ref(func, span)?, Value::String(s, QuoteKind::Quoted) => self.visit_quoted_string(false, s), Value::String(s, QuoteKind::None) => self.visit_unquoted_string(s), Value::ArgList(arglist) => self.visit_arglist(arglist, span)?, } Ok(()) } fn write_style(&mut self, style: Style) -> SassResult<()> { if !self.options.is_compressed() { self.write_indentation(); } self.buffer .extend_from_slice(style.property.resolve_ref().as_bytes()); self.buffer.push(b':'); // todo: _writeFoldedValue and _writeReindentedValue if !style.declared_as_custom_property && !self.options.is_compressed() { self.buffer.push(b' '); } self.visit_value(&style.value.node, style.value.span)?; Ok(()) } fn write_import(&mut self, import: &str, modifiers: Option) -> SassResult<()> { self.write_indentation(); self.buffer.extend_from_slice(b"@import "); write!(&mut self.buffer, "{}", import)?; if let Some(modifiers) = modifiers { self.buffer.push(b' '); self.buffer.extend_from_slice(modifiers.as_bytes()); } Ok(()) } fn write_comment(&mut self, comment: &str, span: Span) -> SassResult<()> { if self.options.is_compressed() && !comment.starts_with("/*!") { return Ok(()); } self.write_indentation(); let col = self.map.look_up_pos(span.low()).position.column; let mut lines = comment.lines(); if let Some(line) = lines.next() { self.buffer.extend_from_slice(line.trim_start().as_bytes()); } let lines = lines .map(|line| { let diff = (line.len() - line.trim_start().len()).saturating_sub(col); format!("{}{}", " ".repeat(diff), line.trim_start()) }) .collect::>() .join("\n"); if !lines.is_empty() { write!(&mut self.buffer, "\n{}", lines)?; } Ok(()) } pub fn requires_semicolon(stmt: &CssStmt) -> bool { match stmt { CssStmt::Style(_) | CssStmt::Import(_, _) => true, CssStmt::UnknownAtRule(rule, _) => !rule.has_body, _ => false, } } fn write_children(&mut self, mut children: Vec) -> SassResult<()> { if self.options.is_compressed() { self.buffer.push(b'{'); } else { self.buffer.extend_from_slice(b" {\n"); } self.indentation += self.indent_width; let last = children.pop(); for child in children { let needs_semicolon = Self::requires_semicolon(&child); let did_write = self.visit_stmt(child)?; if !did_write { continue; } if needs_semicolon { self.buffer.push(b';'); } self.write_optional_newline(); } if let Some(last) = last { let needs_semicolon = Self::requires_semicolon(&last); let did_write = self.visit_stmt(last)?; if did_write { if needs_semicolon && !self.options.is_compressed() { self.buffer.push(b';'); } self.write_optional_newline(); } } self.indentation -= self.indent_width; if self.options.is_compressed() { self.buffer.push(b'}'); } else { self.write_indentation(); self.buffer.extend_from_slice(b"}"); } Ok(()) } fn write_optional_space(&mut self) { if !self.options.is_compressed() { self.buffer.push(b' '); } } fn write_optional_newline(&mut self) { if !self.options.is_compressed() { self.buffer.push(b'\n'); } } fn write_supports_rule(&mut self, supports_rule: SupportsRule) -> SassResult<()> { self.write_indentation(); self.buffer.extend_from_slice(b"@supports"); if !supports_rule.params.is_empty() { self.buffer.push(b' '); self.buffer .extend_from_slice(supports_rule.params.as_bytes()); } self.write_children(supports_rule.body)?; Ok(()) } /// Returns whether or not text was written fn visit_stmt(&mut self, stmt: CssStmt) -> SassResult { if stmt.is_invisible() { return Ok(false); } match stmt { CssStmt::RuleSet { selector, body, .. } => { self.write_indentation(); self.write_selector_list(&selector.as_selector_list()); self.write_children(body)?; } CssStmt::Media(media_rule, ..) => { self.write_indentation(); self.buffer.extend_from_slice(b"@media "); if let Some((last, rest)) = media_rule.query.split_last() { for query in rest { self.write_media_query(query); self.buffer.push(b','); self.write_optional_space(); } self.write_media_query(last); } self.write_children(media_rule.body)?; } CssStmt::UnknownAtRule(unknown_at_rule, ..) => { self.write_indentation(); self.buffer.push(b'@'); self.buffer .extend_from_slice(unknown_at_rule.name.as_bytes()); if !unknown_at_rule.params.is_empty() { write!(&mut self.buffer, " {}", unknown_at_rule.params)?; } if !unknown_at_rule.has_body { debug_assert!(unknown_at_rule.body.is_empty()); return Ok(true); } else if unknown_at_rule.body.iter().all(CssStmt::is_invisible) { self.buffer.extend_from_slice(b" {}"); return Ok(true); } self.write_children(unknown_at_rule.body)?; } CssStmt::Style(style) => self.write_style(style)?, CssStmt::Comment(comment, span) => self.write_comment(&comment, span)?, CssStmt::KeyframesRuleSet(keyframes_rule_set) => { self.write_indentation(); // todo: i bet we can do something like write_with_separator to avoid extra allocation let selector = keyframes_rule_set .selector .into_iter() .map(|s| s.to_string()) .collect::>() .join(", "); self.buffer.extend_from_slice(selector.as_bytes()); self.write_children(keyframes_rule_set.body)?; } CssStmt::Import(import, modifier) => self.write_import(&import, modifier)?, CssStmt::Supports(supports_rule, _) => self.write_supports_rule(supports_rule)?, } Ok(true) } } grass-0.13.4/crates/compiler/src/unit/000077500000000000000000000000001465374720000175675ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/unit/conversion.rs000066400000000000000000000155571465374720000223370ustar00rootroot00000000000000//! A big dictionary of units and their conversion ratios. //! //! Arbitrary precision is retained. use std::{ collections::{HashMap, HashSet}, f64::consts::PI, iter::FromIterator, }; use once_cell::sync::Lazy; use crate::unit::Unit; pub(crate) static UNIT_CONVERSION_TABLE: Lazy>> = Lazy::new(|| { let mut from_in = HashMap::new(); from_in.insert(Unit::In, 1.0); from_in.insert(Unit::Cm, 1.0 / 2.54); from_in.insert(Unit::Pc, 1.0 / 6.0); from_in.insert(Unit::Mm, 1.0 / 25.4); from_in.insert(Unit::Q, 1.0 / 101.6); from_in.insert(Unit::Pt, 1.0 / 72.0); from_in.insert(Unit::Px, 1.0 / 96.0); let mut from_cm = HashMap::new(); from_cm.insert(Unit::In, 2.54); from_cm.insert(Unit::Cm, 1.0); from_cm.insert(Unit::Pc, 2.54 / 6.0); from_cm.insert(Unit::Mm, 1.0 / 10.0); from_cm.insert(Unit::Q, 1.0 / 40.0); from_cm.insert(Unit::Pt, 2.54 / 72.0); from_cm.insert(Unit::Px, 2.54 / 96.0); let mut from_pc = HashMap::new(); from_pc.insert(Unit::In, 6.0); from_pc.insert(Unit::Cm, 6.0 / 2.54); from_pc.insert(Unit::Pc, 1.0); from_pc.insert(Unit::Mm, 6.0 / 25.4); from_pc.insert(Unit::Q, 6.0 / 101.6); from_pc.insert(Unit::Pt, 1.0 / 12.0); from_pc.insert(Unit::Px, 1.0 / 16.0); let mut from_mm = HashMap::new(); from_mm.insert(Unit::In, 25.4); from_mm.insert(Unit::Cm, 10.0); from_mm.insert(Unit::Pc, 25.4 / 6.0); from_mm.insert(Unit::Mm, 1.0); from_mm.insert(Unit::Q, 1.0 / 4.0); from_mm.insert(Unit::Pt, 25.4 / 72.0); from_mm.insert(Unit::Px, 25.4 / 96.0); let mut from_q = HashMap::new(); from_q.insert(Unit::In, 101.6); from_q.insert(Unit::Cm, 40.0); from_q.insert(Unit::Pc, 101.6 / 6.0); from_q.insert(Unit::Mm, 4.0); from_q.insert(Unit::Q, 1.0); from_q.insert(Unit::Pt, 101.6 / 72.0); from_q.insert(Unit::Px, 101.6 / 96.0); let mut from_pt = HashMap::new(); from_pt.insert(Unit::In, 72.0); from_pt.insert(Unit::Cm, 72.0 / 2.54); from_pt.insert(Unit::Pc, 12.0); from_pt.insert(Unit::Mm, 72.0 / 25.4); from_pt.insert(Unit::Q, 72.0 / 101.6); from_pt.insert(Unit::Pt, 1.0); from_pt.insert(Unit::Px, 3.0 / 4.0); let mut from_px = HashMap::new(); from_px.insert(Unit::In, 96.0); from_px.insert(Unit::Cm, 96.0 / 2.54); from_px.insert(Unit::Pc, 16.0); from_px.insert(Unit::Mm, 96.0 / 25.4); from_px.insert(Unit::Q, 96.0 / 101.6); from_px.insert(Unit::Pt, 4.0 / 3.0); from_px.insert(Unit::Px, 1.0); let mut from_deg = HashMap::new(); from_deg.insert(Unit::Deg, 1.0); from_deg.insert(Unit::Grad, 9.0 / 10.0); from_deg.insert(Unit::Rad, 180.0 / PI); from_deg.insert(Unit::Turn, 360.0); let mut from_grad = HashMap::new(); from_grad.insert(Unit::Deg, 10.0 / 9.0); from_grad.insert(Unit::Grad, 1.0); from_grad.insert(Unit::Rad, 200.0 / PI); from_grad.insert(Unit::Turn, 400.0); let mut from_rad = HashMap::new(); from_rad.insert(Unit::Deg, PI / 180.0); from_rad.insert(Unit::Grad, PI / 200.0); from_rad.insert(Unit::Rad, 1.0); from_rad.insert(Unit::Turn, 2.0 * PI); let mut from_turn = HashMap::new(); from_turn.insert(Unit::Deg, 1.0 / 360.0); from_turn.insert(Unit::Grad, 1.0 / 400.0); from_turn.insert(Unit::Rad, 1.0 / (2.0 * PI)); from_turn.insert(Unit::Turn, 1.0); let mut from_s = HashMap::new(); from_s.insert(Unit::S, 1.0); from_s.insert(Unit::Ms, 1.0 / 1000.0); let mut from_ms = HashMap::new(); from_ms.insert(Unit::S, 1000.0); from_ms.insert(Unit::Ms, 1.0); let mut from_hz = HashMap::new(); from_hz.insert(Unit::Hz, 1.0); from_hz.insert(Unit::Khz, 1000.0); let mut from_khz = HashMap::new(); from_khz.insert(Unit::Hz, 1.0 / 1000.0); from_khz.insert(Unit::Khz, 1.0); let mut from_dpi = HashMap::new(); from_dpi.insert(Unit::Dpi, 1.0); from_dpi.insert(Unit::Dpcm, 2.54); from_dpi.insert(Unit::Dppx, 96.0); let mut from_dpcm = HashMap::new(); from_dpcm.insert(Unit::Dpi, 1.0 / 2.54); from_dpcm.insert(Unit::Dpcm, 1.0); from_dpcm.insert(Unit::Dppx, 96.0 / 2.54); let mut from_dppx = HashMap::new(); from_dppx.insert(Unit::Dpi, 1.0 / 96.0); from_dppx.insert(Unit::Dpcm, 2.54 / 96.0); from_dppx.insert(Unit::Dppx, 1.0); let mut m = HashMap::new(); m.insert(Unit::In, from_in); m.insert(Unit::Cm, from_cm); m.insert(Unit::Pc, from_pc); m.insert(Unit::Mm, from_mm); m.insert(Unit::Q, from_q); m.insert(Unit::Pt, from_pt); m.insert(Unit::Px, from_px); m.insert(Unit::Deg, from_deg); m.insert(Unit::Grad, from_grad); m.insert(Unit::Rad, from_rad); m.insert(Unit::Turn, from_turn); m.insert(Unit::S, from_s); m.insert(Unit::Ms, from_ms); m.insert(Unit::Hz, from_hz); m.insert(Unit::Khz, from_khz); m.insert(Unit::Dpi, from_dpi); m.insert(Unit::Dpcm, from_dpcm); m.insert(Unit::Dppx, from_dppx); m }); pub(crate) static KNOWN_COMPATIBILITIES: Lazy<[HashSet; 5]> = Lazy::new(|| { let dimensions = HashSet::from_iter([ Unit::Em, Unit::Ex, Unit::Ch, Unit::Rem, Unit::Vw, Unit::Vh, Unit::Vmin, Unit::Vmax, Unit::Cm, Unit::Mm, Unit::Q, Unit::In, Unit::Pt, Unit::Pc, Unit::Px, ]); let angles = HashSet::from_iter([Unit::Deg, Unit::Grad, Unit::Rad, Unit::Turn]); let time = HashSet::from_iter([Unit::S, Unit::Ms]); let frequency = HashSet::from_iter([Unit::Hz, Unit::Khz]); let resolution = HashSet::from_iter([Unit::Dpi, Unit::Dpcm, Unit::Dppx]); [dimensions, angles, time, frequency, resolution] }); pub(crate) fn known_compatibilities_by_unit(unit: &Unit) -> Option<&HashSet> { match unit { Unit::Em | Unit::Ex | Unit::Ch | Unit::Rem | Unit::Vw | Unit::Vh | Unit::Vmin | Unit::Vmax | Unit::Cm | Unit::Mm | Unit::Q | Unit::In | Unit::Pt | Unit::Pc | Unit::Px => Some(&KNOWN_COMPATIBILITIES[0]), Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => Some(&KNOWN_COMPATIBILITIES[1]), Unit::S | Unit::Ms => Some(&KNOWN_COMPATIBILITIES[2]), Unit::Hz | Unit::Khz => Some(&KNOWN_COMPATIBILITIES[3]), Unit::Dpi | Unit::Dpcm | Unit::Dppx => Some(&KNOWN_COMPATIBILITIES[4]), _ => None, } } grass-0.13.4/crates/compiler/src/unit/mod.rs000066400000000000000000000225541465374720000207240ustar00rootroot00000000000000use std::{fmt, sync::Arc}; use crate::interner::InternedString; pub(crate) use conversion::{known_compatibilities_by_unit, UNIT_CONVERSION_TABLE}; mod conversion; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Unit { // Absolute units /// Pixels Px, /// Millimeters Mm, /// Inches In, /// Centimeters Cm, /// Quarter-millimeters Q, /// Points Pt, /// Picas Pc, // Font relative units /// Font size of the parent element Em, /// Font size of the root element Rem, /// Line height of the element Lh, /// x-height of the element's font Ex, /// The advance measure (width) of the glyph "0" of the element's font Ch, /// Represents the "cap height" (nominal height of capital letters) of the element's font Cap, /// Equal to the used advance measure of the "水" (CJK water ideograph, U+6C34) glyph /// found in the font used to render it Ic, /// Equal to the computed value of the line-height property on the root element /// (typically \), converted to an absolute length Rlh, // Viewport relative units /// 1% of the viewport's width Vw, /// 1% of the viewport's height Vh, /// 1% of the viewport's smaller dimension Vmin, /// 1% of the viewport's larger dimension Vmax, /// Equal to 1% of the size of the initial containing block, in the direction of the root /// element's inline axis Vi, /// Equal to 1% of the size of the initial containing block, in the direction of the root /// element's block axis Vb, // Angle units /// Represents an angle in degrees. One full circle is 360deg Deg, /// Represents an angle in gradians. One full circle is 400grad Grad, /// Represents an angle in radians. One full circle is 2π radians which approximates to 6.283rad Rad, /// Represents an angle in a number of turns. One full circle is 1turn Turn, // Time units /// Represents a time in seconds S, /// Represents a time in milliseconds Ms, // Frequency units /// Represents a frequency in hertz Hz, /// Represents a frequency in kilohertz Khz, // Resolution units /// Represents the number of dots per inch Dpi, /// Represents the number of dots per centimeter Dpcm, /// Represents the number of dots per px unit Dppx, // Other units /// Represents a fraction of the available space in the grid container Fr, Percent, /// Unknown unit Unknown(InternedString), /// Unspecified unit None, Complex(Arc), } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ComplexUnit { pub numer: Vec, pub denom: Vec, } pub(crate) fn are_any_convertible(units1: &[Unit], units2: &[Unit]) -> bool { for unit1 in units1 { for unit2 in units2 { if unit1.comparable(unit2) { return true; } } } false } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum UnitKind { Absolute, FontRelative, ViewportRelative, Angle, Time, Frequency, Resolution, Other, None, } impl Unit { pub(crate) fn new(mut numer: Vec, denom: Vec) -> Self { if denom.is_empty() && numer.is_empty() { Unit::None } else if denom.is_empty() && numer.len() == 1 { numer.pop().unwrap() } else { Unit::Complex(Arc::new(ComplexUnit { numer, denom })) } } pub(crate) fn numer_and_denom(self) -> (Vec, Vec) { match self { Self::Complex(complex) => (complex.numer.clone(), complex.denom.clone()), Self::None => (Vec::new(), Vec::new()), v => (vec![v], Vec::new()), } } pub(crate) fn invert(self) -> Self { let (numer, denom) = self.numer_and_denom(); Self::new(denom, numer) } pub(crate) fn is_complex(&self) -> bool { matches!(self, Unit::Complex(complex) if complex.numer.len() != 1 || !complex.denom.is_empty()) } pub(crate) fn comparable(&self, other: &Unit) -> bool { if other == &Unit::None { return true; } match self.kind() { UnitKind::FontRelative | UnitKind::ViewportRelative | UnitKind::Other => self == other, UnitKind::None => true, u => other.kind() == u, } } /// Used internally to determine if two units are comparable or not fn kind(&self) -> UnitKind { match self { Unit::Px | Unit::Mm | Unit::In | Unit::Cm | Unit::Q | Unit::Pt | Unit::Pc => { UnitKind::Absolute } Unit::Em | Unit::Rem | Unit::Lh | Unit::Ex | Unit::Ch | Unit::Cap | Unit::Ic | Unit::Rlh => UnitKind::FontRelative, Unit::Vw | Unit::Vh | Unit::Vmin | Unit::Vmax | Unit::Vi | Unit::Vb => { UnitKind::ViewportRelative } Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => UnitKind::Angle, Unit::S | Unit::Ms => UnitKind::Time, Unit::Hz | Unit::Khz => UnitKind::Frequency, Unit::Dpi | Unit::Dpcm | Unit::Dppx => UnitKind::Resolution, Unit::None => UnitKind::None, Unit::Fr | Unit::Percent | Unit::Unknown(..) | Unit::Complex { .. } => UnitKind::Other, } } } impl From for Unit { fn from(unit: String) -> Self { match unit.to_ascii_lowercase().as_str() { "px" => Unit::Px, "mm" => Unit::Mm, "in" => Unit::In, "cm" => Unit::Cm, "q" => Unit::Q, "pt" => Unit::Pt, "pc" => Unit::Pc, "em" => Unit::Em, "rem" => Unit::Rem, "lh" => Unit::Lh, "%" => Unit::Percent, "ex" => Unit::Ex, "ch" => Unit::Ch, "cap" => Unit::Cap, "ic" => Unit::Ic, "rlh" => Unit::Rlh, "vw" => Unit::Vw, "vh" => Unit::Vh, "vmin" => Unit::Vmin, "vmax" => Unit::Vmax, "vi" => Unit::Vi, "vb" => Unit::Vb, "deg" => Unit::Deg, "grad" => Unit::Grad, "rad" => Unit::Rad, "turn" => Unit::Turn, "s" => Unit::S, "ms" => Unit::Ms, "hz" => Unit::Hz, "khz" => Unit::Khz, "dpi" => Unit::Dpi, "dpcm" => Unit::Dpcm, "dppx" => Unit::Dppx, "fr" => Unit::Fr, _ => Unit::Unknown(InternedString::get_or_intern(unit)), } } } impl fmt::Display for Unit { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Unit::Px => write!(f, "px"), Unit::Mm => write!(f, "mm"), Unit::In => write!(f, "in"), Unit::Cm => write!(f, "cm"), Unit::Q => write!(f, "q"), Unit::Pt => write!(f, "pt"), Unit::Pc => write!(f, "pc"), Unit::Em => write!(f, "em"), Unit::Rem => write!(f, "rem"), Unit::Lh => write!(f, "lh"), Unit::Percent => write!(f, "%"), Unit::Ex => write!(f, "ex"), Unit::Ch => write!(f, "ch"), Unit::Cap => write!(f, "cap"), Unit::Ic => write!(f, "ic"), Unit::Rlh => write!(f, "rlh"), Unit::Vw => write!(f, "vw"), Unit::Vh => write!(f, "vh"), Unit::Vmin => write!(f, "vmin"), Unit::Vmax => write!(f, "vmax"), Unit::Vi => write!(f, "vi"), Unit::Vb => write!(f, "vb"), Unit::Deg => write!(f, "deg"), Unit::Grad => write!(f, "grad"), Unit::Rad => write!(f, "rad"), Unit::Turn => write!(f, "turn"), Unit::S => write!(f, "s"), Unit::Ms => write!(f, "ms"), Unit::Hz => write!(f, "Hz"), Unit::Khz => write!(f, "kHz"), Unit::Dpi => write!(f, "dpi"), Unit::Dpcm => write!(f, "dpcm"), Unit::Dppx => write!(f, "dppx"), Unit::Fr => write!(f, "fr"), Unit::Unknown(s) => write!(f, "{}", s), Unit::None => Ok(()), Unit::Complex(complex) => { let numer = &complex.numer; let denom = &complex.denom; debug_assert!( numer.len() > 1 || !denom.is_empty(), "unsimplified complex unit" ); let numer_rendered = numer .iter() .map(ToString::to_string) .collect::>() .join("*"); let denom_rendered = denom .iter() .map(ToString::to_string) .collect::>() .join("*"); if denom.is_empty() { write!(f, "{}", numer_rendered) } else if numer.is_empty() && denom.len() == 1 { write!(f, "{}^-1", denom_rendered) } else if numer.is_empty() { write!(f, "({})^-1", denom_rendered) } else { write!(f, "{}/{}", numer_rendered, denom_rendered) } } } } } grass-0.13.4/crates/compiler/src/utils/000077500000000000000000000000001465374720000177505ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/utils/chars.rs000066400000000000000000000012121465374720000214120ustar00rootroot00000000000000pub(crate) fn hex_char_for(number: u32) -> char { debug_assert!(number < 0x10); std::char::from_u32(if number < 0xA { 0x30 + number } else { 0x61 - 0xA + number }) .unwrap() } pub(crate) fn is_name(c: char) -> bool { is_name_start(c) || c.is_ascii_digit() || c == '-' } pub(crate) fn is_name_start(c: char) -> bool { c == '_' || c.is_alphabetic() || c as u32 >= 0x0080 } pub(crate) fn as_hex(c: char) -> u32 { match c { '0'..='9' => c as u32 - '0' as u32, 'A'..='F' => 10 + c as u32 - 'A' as u32, 'a'..='f' => 10 + c as u32 - 'a' as u32, _ => unreachable!(), } } grass-0.13.4/crates/compiler/src/utils/map_view.rs000066400000000000000000000230111465374720000221220ustar00rootroot00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, fmt, sync::Arc, }; use crate::common::Identifier; pub(crate) trait MapView: fmt::Debug { type Value; fn get(&self, name: Identifier) -> Option; fn remove(&self, name: Identifier) -> Option; fn insert(&self, name: Identifier, value: Self::Value) -> Option; fn len(&self) -> usize; fn is_empty(&self) -> bool { self.len() == 0 } fn contains_key(&self, k: Identifier) -> bool { self.get(k).is_some() } // todo: wildly ineffecient to return vec here, because of the arbitrary nesting of Self fn keys(&self) -> Vec; fn iter(&self) -> Vec<(Identifier, Self::Value)>; } impl MapView for Arc> { type Value = T; fn get(&self, name: Identifier) -> Option { (**self).get(name) } fn remove(&self, name: Identifier) -> Option { (**self).remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { (**self).insert(name, value) } fn len(&self) -> usize { (**self).len() } fn keys(&self) -> Vec { (**self).keys() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { (**self).iter() } } #[derive(Debug)] pub(crate) struct BaseMapView(pub Arc>>); impl Clone for BaseMapView { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } } #[derive(Debug, Clone)] pub(crate) struct UnprefixedMapView + Clone>( pub T, pub String, ); #[derive(Debug, Clone)] pub(crate) struct PrefixedMapView + Clone>( pub T, pub String, ); impl MapView for BaseMapView { type Value = T; fn get(&self, name: Identifier) -> Option { (*self.0).borrow().get(&name).cloned() } fn len(&self) -> usize { (*self.0).borrow().len() } fn remove(&self, name: Identifier) -> Option { (*self.0).borrow_mut().remove(&name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { (*self.0).borrow_mut().insert(name, value) } fn keys(&self) -> Vec { (*self.0).borrow().keys().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { (*self.0).borrow().clone().into_iter().collect() } } impl + Clone> MapView for UnprefixedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.get(name) } fn remove(&self, name: Identifier) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .into_iter() .filter(|key| key.as_str().starts_with(&self.1)) .map(|key| Identifier::from(key.as_str().strip_prefix(&self.1).unwrap())) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } impl + Clone> MapView for PrefixedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .into_iter() .filter(|key| key.as_str().starts_with(&self.1)) .map(|key| Identifier::from(format!("{}{}", self.1, key))) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } /// A mostly-unmodifiable view of a map that only allows certain keys to be /// accessed. /// /// Whether or not the underlying map contains keys that aren't allowed, this /// view will behave as though it doesn't contain them. /// /// The underlying map's values may change independently of this view, but its /// set of keys may not. /// /// This is unmodifiable *except for the [remove] method*, which is used for /// `@used with` to mark configured variables as used. #[derive(Debug, Clone)] pub(crate) struct LimitedMapView + Clone>( pub T, pub HashSet, ); impl + Clone> LimitedMapView { pub fn safelist(map: T, keys: &HashSet) -> Self { let keys = keys .iter() .copied() .filter(|key| map.contains_key(*key)) .collect(); Self(map, keys) } pub fn blocklist(map: T, blocklist: &HashSet) -> Self { let keys = map .keys() .into_iter() .filter(|key| !blocklist.contains(key)) .collect(); Self(map, keys) } } impl + Clone> MapView for LimitedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !self.1.contains(&name) { return None; } self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !self.1.contains(&name) { return None; } self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !self.1.contains(&name) { return None; } self.0.insert(name, value) } fn len(&self) -> usize { self.1.len() } fn keys(&self) -> Vec { self.1.iter().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } #[derive(Debug)] pub(crate) struct MergedMapView( pub Vec>>, HashSet, ); impl MergedMapView { pub fn new(maps: Vec>>) -> Self { let unique_keys: HashSet = maps.iter().fold(HashSet::new(), |mut keys, map| { keys.extend(&map.keys()); keys }); Self(maps, unique_keys) } } impl MapView for MergedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { self.0.iter().rev().find_map(|map| (*map).get(name)) } fn remove(&self, _name: Identifier) -> Option { unimplemented!() } fn len(&self) -> usize { self.1.len() } fn insert(&self, name: Identifier, value: Self::Value) -> Option { for map in self.0.iter().rev() { if map.contains_key(name) { return map.insert(name, value); } } unreachable!("New entries may not be added to MergedMapView") } fn keys(&self) -> Vec { self.1.iter().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { self.1 .iter() .copied() .map(|name| (name, self.get(name).unwrap())) .collect() } } #[derive(Debug, Clone)] pub(crate) struct PublicMemberMapView + Clone>(pub T); impl + Clone> MapView for PublicMemberMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !name.is_public() { return None; } self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !name.is_public() { return None; } self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !name.is_public() { return None; } self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .iter() .copied() .filter(Identifier::is_public) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { self.0 .iter() .into_iter() .filter(|(name, _)| Identifier::is_public(name)) .collect() } } grass-0.13.4/crates/compiler/src/utils/mod.rs000066400000000000000000000045501465374720000211010ustar00rootroot00000000000000pub(crate) use chars::*; pub(crate) use map_view::*; pub(crate) use strings::*; mod chars; mod map_view; mod strings; #[allow(clippy::case_sensitive_file_extension_comparisons)] pub(crate) fn is_plain_css_import(url: &str) -> bool { if url.len() < 5 { return false; } let lower = url.to_ascii_lowercase(); lower.ends_with(".css") || lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("//") } pub(crate) fn opposite_bracket(b: char) -> char { debug_assert!(matches!(b, '(' | '{' | '[' | ')' | '}' | ']')); match b { '(' => ')', '{' => '}', '[' => ']', ')' => '(', '}' => '{', ']' => '[', _ => unreachable!(), } } pub(crate) fn is_special_function(s: &str) -> bool { s.starts_with("calc(") || s.starts_with("var(") || s.starts_with("env(") || s.starts_with("min(") || s.starts_with("max(") || s.starts_with("clamp(") } /// Trim ASCII whitespace from both sides of string. /// /// If [excludeEscape] is `true`, this doesn't trim whitespace included in a CSS /// escape. pub(crate) fn trim_ascii( s: &str, // default=false exclude_escape: bool, ) -> &str { match s.chars().position(|c| !c.is_ascii_whitespace()) { Some(start) => &s[start..=last_non_whitespace(s, exclude_escape).unwrap()], None => "", } } fn last_non_whitespace(s: &str, exclude_escape: bool) -> Option { let mut idx = s.len() - 1; for c in s.chars().rev() { if !c.is_ascii_whitespace() { if exclude_escape && idx != 0 && idx != s.len() - 1 && c == '\\' { return Some(idx + 1); } else { return Some(idx); } } idx -= 1; } None } pub(crate) fn to_sentence>(mut elems: Vec, conjunction: &'static str) -> String { debug_assert!( !elems.is_empty(), "expected sentence to contain at least one element" ); if elems.len() == 1 { return elems.pop().unwrap().into(); } let last = elems.pop().unwrap(); format!( "{} {conjunction} {}", elems .into_iter() .map(Into::into) .collect::>() .join(", "), last.into(), conjunction = conjunction, ) } grass-0.13.4/crates/compiler/src/utils/strings.rs000066400000000000000000000015551465374720000220150ustar00rootroot00000000000000use super::{is_name, is_name_start}; pub(crate) fn is_ident(s: &str) -> bool { let mut chars = s.chars().peekable(); match chars.next() { Some(c) if is_name_start(c) && !c.is_numeric() => {} Some(..) | None => return false, } while let Some(c) = chars.next() { if c == '\\' { for _ in 0..6 { let next = match chars.next() { Some(t) => t, None => return true, }; if !next.is_ascii_hexdigit() { break; } } match chars.peek() { Some(c) if c.is_whitespace() => { chars.next(); } _ => {} }; continue; } if !is_name(c) { return false; } } true } grass-0.13.4/crates/compiler/src/value/000077500000000000000000000000001465374720000177245ustar00rootroot00000000000000grass-0.13.4/crates/compiler/src/value/arglist.rs000066400000000000000000000031611465374720000217400ustar00rootroot00000000000000use std::{cell::Cell, collections::BTreeMap, rc::Rc}; use crate::common::{Identifier, ListSeparator}; use super::Value; #[derive(Debug, Clone)] pub struct ArgList { pub elems: Vec, were_keywords_accessed: Rc>, // todo: special wrapper around this field to avoid having to make it private? keywords: BTreeMap, pub separator: ListSeparator, } impl PartialEq for ArgList { fn eq(&self, other: &Self) -> bool { self.elems == other.elems && self.keywords == other.keywords && self.separator == other.separator } } impl Eq for ArgList {} impl ArgList { pub fn new( elems: Vec, were_keywords_accessed: Rc>, keywords: BTreeMap, separator: ListSeparator, ) -> Self { debug_assert!( !(*were_keywords_accessed).get(), "expected args to initialize with unaccessed keywords" ); Self { elems, were_keywords_accessed, keywords, separator, } } pub fn len(&self) -> usize { self.elems.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn is_blank(&self) -> bool { !self.is_empty() && (self.elems.iter().all(Value::is_blank)) } pub fn keywords(&self) -> &BTreeMap { (*self.were_keywords_accessed).set(true); &self.keywords } pub fn into_keywords(self) -> BTreeMap { (*self.were_keywords_accessed).set(true); self.keywords } } grass-0.13.4/crates/compiler/src/value/calculation.rs000066400000000000000000000307201465374720000225720ustar00rootroot00000000000000use core::fmt; use std::iter::Iterator; use codemap::Span; use crate::{ common::BinaryOp, error::SassResult, serializer::inspect_number, unit::Unit, value::{SassNumber, Value}, Options, }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalculationArg { Number(SassNumber), Calculation(SassCalculation), String(String), Operation { lhs: Box, op: BinaryOp, rhs: Box, }, Interpolation(String), } impl CalculationArg { pub fn parenthesize_calculation_rhs(outer: BinaryOp, right: BinaryOp) -> bool { if outer == BinaryOp::Div { true } else if outer == BinaryOp::Plus { false } else { right == BinaryOp::Plus || right == BinaryOp::Minus } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CalculationName { Calc, Min, Max, Clamp, } impl fmt::Display for CalculationName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CalculationName::Calc => f.write_str("calc"), CalculationName::Min => f.write_str("min"), CalculationName::Max => f.write_str("max"), CalculationName::Clamp => f.write_str("clamp"), } } } impl CalculationName { pub(crate) fn in_min_or_max(self) -> bool { self == CalculationName::Min || self == CalculationName::Max } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SassCalculation { pub name: CalculationName, pub args: Vec, } impl SassCalculation { pub fn unsimplified(name: CalculationName, args: Vec) -> Self { Self { name, args } } pub fn calc(arg: CalculationArg) -> Value { let arg = Self::simplify(arg); match arg { CalculationArg::Number(n) => Value::Dimension(n), CalculationArg::Calculation(c) => Value::Calculation(c), _ => Value::Calculation(SassCalculation { name: CalculationName::Calc, args: vec![arg], }), } } pub fn min(args: Vec, options: &Options, span: Span) -> SassResult { let args = Self::simplify_arguments(args); debug_assert!(!args.is_empty(), "min() must have at least one argument."); let mut minimum: Option = None; for arg in &args { match arg { CalculationArg::Number(n) if minimum.is_some() && !minimum.as_ref().unwrap().is_comparable_to(n) => { minimum = None; break; } CalculationArg::Number(n) if minimum.is_none() || minimum.as_ref().unwrap().num > n.num.convert(&n.unit, &minimum.as_ref().unwrap().unit) => { minimum = Some(n.clone()); } CalculationArg::Number(..) => continue, _ => { minimum = None; break; } } } Ok(match minimum { Some(min) => Value::Dimension(min), None => { Self::verify_compatible_numbers(&args, options, span)?; Value::Calculation(SassCalculation { name: CalculationName::Min, args, }) } }) } pub fn max(args: Vec, options: &Options, span: Span) -> SassResult { let args = Self::simplify_arguments(args); if args.is_empty() { return Err(("max() must have at least one argument.", span).into()); } let mut maximum: Option = None; for arg in &args { match arg { CalculationArg::Number(n) if maximum.is_some() && !maximum.as_ref().unwrap().is_comparable_to(n) => { maximum = None; break; } CalculationArg::Number(n) if maximum.is_none() || maximum.as_ref().unwrap().num < n.num.convert(&n.unit, &maximum.as_ref().unwrap().unit) => { maximum = Some(n.clone()); } CalculationArg::Number(..) => continue, _ => { maximum = None; break; } } } Ok(match maximum { Some(max) => Value::Dimension(max), None => { Self::verify_compatible_numbers(&args, options, span)?; Value::Calculation(SassCalculation { name: CalculationName::Max, args, }) } }) } pub fn clamp( min: CalculationArg, value: Option, max: Option, options: &Options, span: Span, ) -> SassResult { if value.is_none() && max.is_some() { return Err(("If value is null, max must also be null.", span).into()); } let min = Self::simplify(min); let value = value.map(Self::simplify); let max = max.map(Self::simplify); match (min.clone(), value.clone(), max.clone()) { ( CalculationArg::Number(min), Some(CalculationArg::Number(value)), Some(CalculationArg::Number(max)), ) => { if min.is_comparable_to(&value) && min.is_comparable_to(&max) { if value.num <= min.num.convert(min.unit(), value.unit()) { return Ok(Value::Dimension(min)); } if value.num >= max.num.convert(max.unit(), value.unit()) { return Ok(Value::Dimension(max)); } return Ok(Value::Dimension(value)); } } _ => {} } let mut args = vec![min]; if let Some(value) = value { args.push(value); } if let Some(max) = max { args.push(max); } Self::verify_length(&args, 3, span)?; Self::verify_compatible_numbers(&args, options, span)?; Ok(Value::Calculation(SassCalculation { name: CalculationName::Clamp, args, })) } fn verify_length(args: &[CalculationArg], len: usize, span: Span) -> SassResult<()> { if args.len() == len { return Ok(()); } if args.iter().any(|arg| { matches!( arg, CalculationArg::String(..) | CalculationArg::Interpolation(..) ) }) { return Ok(()); } let was_or_were = if args.len() == 1 { "was" } else { "were" }; Err(( format!( "{len} arguments required, but only {} {was_or_were} passed.", args.len(), len = len, was_or_were = was_or_were, ), span, ) .into()) } #[allow(clippy::needless_range_loop)] fn verify_compatible_numbers( args: &[CalculationArg], options: &Options, span: Span, ) -> SassResult<()> { for arg in args { match arg { CalculationArg::Number(num) => match &num.unit { Unit::Complex(complex) => { if complex.numer.len() > 1 || !complex.denom.is_empty() { let num = num.clone(); let value = Value::Dimension(num); return Err(( format!( "Number {} isn't compatible with CSS calculations.", value.inspect(span)? ), span, ) .into()); } } _ => continue, }, _ => continue, } } for i in 0..args.len() { let number1 = match &args[i] { CalculationArg::Number(num) => num, _ => continue, }; for j in (i + 1)..args.len() { let number2 = match &args[j] { CalculationArg::Number(num) => num, _ => continue, }; if number1.has_possibly_compatible_units(number2) { continue; } return Err(( format!( "{} and {} are incompatible.", inspect_number(number1, options, span)?, inspect_number(number2, options, span)? ), span, ) .into()); } } Ok(()) } pub fn operate_internal( mut op: BinaryOp, left: CalculationArg, right: CalculationArg, in_min_or_max: bool, simplify: bool, options: &Options, span: Span, ) -> SassResult { if !simplify { return Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }); } let left = Self::simplify(left); let mut right = Self::simplify(right); if op == BinaryOp::Plus || op == BinaryOp::Minus { match (&left, &right) { (CalculationArg::Number(left), CalculationArg::Number(right)) if if in_min_or_max { left.is_comparable_to(right) } else { left.has_compatible_units(&right.unit) } => { if op == BinaryOp::Plus { return Ok(CalculationArg::Number(left.clone() + right.clone())); } else { return Ok(CalculationArg::Number(left.clone() - right.clone())); } } _ => {} } Self::verify_compatible_numbers(&[left.clone(), right.clone()], options, span)?; if let CalculationArg::Number(mut n) = right { if n.num.is_negative() { n.num.0 *= -1.0; op = if op == BinaryOp::Plus { BinaryOp::Minus } else { BinaryOp::Plus } } else { // todo: do we need this branch? } right = CalculationArg::Number(n); } return Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }); } match (left, right) { (CalculationArg::Number(num1), CalculationArg::Number(num2)) => { if op == BinaryOp::Mul { Ok(CalculationArg::Number(num1 * num2)) } else { Ok(CalculationArg::Number(num1 / num2)) } } (left, right) => Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }), } // _verifyCompatibleNumbers([left, right]); // Ok(CalculationArg::Operation { // lhs: Box::new(left), // op, // rhs: Box::new(right), // }) } fn simplify(arg: CalculationArg) -> CalculationArg { match arg { CalculationArg::Number(..) | CalculationArg::Operation { .. } | CalculationArg::Interpolation(..) | CalculationArg::String(..) => arg, CalculationArg::Calculation(mut calc) => { if calc.name == CalculationName::Calc { calc.args.remove(0) } else { CalculationArg::Calculation(calc) } } } } fn simplify_arguments(args: Vec) -> Vec { args.into_iter().map(Self::simplify).collect() } } grass-0.13.4/crates/compiler/src/value/map.rs000066400000000000000000000052331465374720000210520ustar00rootroot00000000000000use std::{slice::Iter, vec::IntoIter}; use codemap::Spanned; use crate::{ common::{Brackets, ListSeparator}, value::Value, }; #[derive(Debug, Clone, Default)] pub struct SassMap(Vec<(Spanned, Value)>); impl PartialEq for SassMap { fn eq(&self, other: &Self) -> bool { if self.0.len() != other.0.len() { return false; } for (key, value) in &self.0 { if !other .0 .iter() .any(|(key2, value2)| key.node == key2.node && value == value2) { return false; } } true } } impl Eq for SassMap {} impl SassMap { pub const fn new() -> SassMap { SassMap(Vec::new()) } pub const fn new_with(elements: Vec<(Spanned, Value)>) -> SassMap { SassMap(elements) } pub fn get(self, key: &Value) -> Option { for (k, v) in self.0 { if &k.node == key { return Some(v); } } None } pub fn get_ref(&self, key: &Value) -> Option<&Value> { for (k, v) in &self.0 { if &k.node == key { return Some(v); } } None } pub fn remove(&mut self, key: &Value) { self.0.retain(|(ref k, ..)| k.not_equals(key)); } pub fn merge(&mut self, other: SassMap) { for (key, value) in other { self.insert(key, value); } } pub fn iter(&self) -> Iter<(Spanned, Value)> { self.0.iter() } pub fn keys(self) -> Vec { self.0.into_iter().map(|(k, ..)| k.node).collect() } pub fn values(self) -> Vec { self.0.into_iter().map(|(.., v)| v).collect() } pub fn contains(&self, key: &Value) -> bool { self.0.iter().any(|(k, ..)| &k.node == key) } pub fn as_list(self) -> Vec { self.0 .into_iter() .map(|(k, v)| Value::List(vec![k.node, v], ListSeparator::Space, Brackets::None)) .collect() } /// Returns true if the key already exists pub fn insert(&mut self, key: Spanned, value: Value) -> bool { for (ref k, ref mut v) in &mut self.0 { if k.node == key.node { *v = value; return true; } } self.0.push((key, value)); false } pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl IntoIterator for SassMap { type Item = (Spanned, Value); type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } grass-0.13.4/crates/compiler/src/value/mod.rs000066400000000000000000000434501465374720000210570ustar00rootroot00000000000000use std::{cmp::Ordering, sync::Arc}; use codemap::{Span, Spanned}; use crate::{ color::Color, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, error::SassResult, evaluate::Visitor, selector::Selector, serializer::{inspect_value, serialize_value}, unit::Unit, utils::is_special_function, Options, OutputStyle, }; pub use arglist::ArgList; pub use calculation::*; pub use map::SassMap; pub use number::*; pub use sass_function::{SassFunction, UserDefinedFunction}; pub(crate) use sass_number::conversion_factor; pub use sass_number::SassNumber; mod arglist; mod calculation; mod map; mod number; mod sass_function; mod sass_number; #[derive(Debug, Clone)] pub enum Value { True, False, Null, Dimension(SassNumber), List(Vec, ListSeparator, Brackets), Color(Arc), String(String, QuoteKind), Map(SassMap), ArgList(ArgList), /// Returned by `get-function()` FunctionRef(Box), Calculation(SassCalculation), } impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match self { Value::Calculation(calc1) => match other { Value::Calculation(calc2) => calc1 == calc2, _ => false, }, Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 == s2, _ => false, }, Value::Dimension(n1) => match other { Value::Dimension(n2) => n1 == n2, _ => false, }, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { false } else { for (a, b) in list1.iter().zip(list2) { if a != b { return false; } } true } } _ => false, }, Value::Null => matches!(other, Value::Null), Value::True => matches!(other, Value::True), Value::False => matches!(other, Value::False), Value::FunctionRef(fn1) => { if let Value::FunctionRef(fn2) = other { fn1 == fn2 } else { false } } Value::Map(map1) => { if let Value::Map(map2) = other { map1 == map2 } else { false } } Value::Color(color1) => { if let Value::Color(color2) = other { color1 == color2 } else { false } } Value::ArgList(list1) => match other { Value::ArgList(list2) => list1 == list2, Value::List(list2, ListSeparator::Comma, ..) => { if list1.len() != list2.len() { return false; } for (el1, el2) in list1.elems.iter().zip(list2) { if el1 != el2 { return false; } } true } _ => false, }, } } } impl Eq for Value {} impl Value { pub fn with_slash( self, numerator: SassNumber, denom: SassNumber, span: Span, ) -> SassResult { let mut number = self.assert_number(span)?; number.as_slash = Some(Arc::new((numerator, denom))); Ok(Value::Dimension(number)) } pub fn assert_number(self, span: Span) -> SassResult { match self { Value::Dimension(n) => Ok(n), _ => Err((format!("{} is not a number.", self.inspect(span)?), span).into()), } } pub fn assert_number_with_name(self, name: &str, span: Span) -> SassResult { match self { Value::Dimension(n) => Ok(n), _ => Err(( format!( "${name}: {} is not a number.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_color_with_name(self, name: &str, span: Span) -> SassResult> { match self { Value::Color(c) => Ok(c), _ => Err(( format!( "${name}: {} is not a color.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_map_with_name(self, name: &str, span: Span) -> SassResult { match self { Value::Map(m) => Ok(m), Value::List(v, ..) if v.is_empty() => Ok(SassMap::new()), Value::ArgList(v) if v.is_empty() => Ok(SassMap::new()), _ => Err(( format!( "${name}: {} is not a map.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_string_with_name( self, name: &str, span: Span, ) -> SassResult<(String, QuoteKind)> { match self { Value::String(s, quotes) => Ok((s, quotes)), _ => Err(( format!( "${name}: {} is not a string.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn is_blank(&self) -> bool { match self { Value::Null => true, Value::String(i, QuoteKind::None) if i.is_empty() => true, Value::List(_, _, Brackets::Bracketed) => false, Value::List(v, ..) => v.iter().all(Value::is_blank), Value::ArgList(v, ..) => v.is_blank(), _ => false, } } pub fn is_empty_list(&self) -> bool { match self { Value::List(v, ..) => v.is_empty(), Value::Map(m) => m.is_empty(), Value::ArgList(v) => v.elems.is_empty(), _ => false, } } pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult { serialize_value( self, &Options::default().style(if is_compressed { OutputStyle::Compressed } else { OutputStyle::Expanded }), span, ) } pub fn inspect(&self, span: Span) -> SassResult { inspect_value(self, &Options::default(), span) } pub fn is_truthy(&self) -> bool { !matches!(self, Value::Null | Value::False) } pub fn unquote(self) -> Self { match self { Value::String(s1, _) => Value::String(s1, QuoteKind::None), Value::List(v, sep, bracket) => { Value::List(v.into_iter().map(Value::unquote).collect(), sep, bracket) } v => v, } } pub const fn span(self, span: Span) -> Spanned { Spanned { node: self, span } } pub fn kind(&self) -> &'static str { match self { Value::Color(..) => "color", Value::String(..) => "string", Value::Calculation(..) => "calculation", Value::Dimension(..) => "number", Value::List(..) => "list", Value::FunctionRef(..) => "function", Value::ArgList(..) => "arglist", Value::True | Value::False => "bool", Value::Null => "null", Value::Map(..) => "map", } } pub fn as_slash(&self) -> Option> { match self { Value::Dimension(SassNumber { as_slash, .. }) => as_slash.clone(), _ => None, } } pub fn without_slash(self) -> Self { match self { Value::Dimension(SassNumber { num, unit, as_slash: _, }) => Value::Dimension(SassNumber { num, unit, as_slash: None, }), _ => self, } } pub fn is_special_function(&self) -> bool { match self { Value::String(s, QuoteKind::None) => is_special_function(s), Value::Calculation(..) => true, _ => false, } } pub fn is_var(&self) -> bool { match self { Value::String(s, QuoteKind::None) => { if s.len() < "var(--_)".len() { return false; } s.starts_with("var(") } Value::Calculation(..) => true, _ => false, } } pub fn try_map(&self) -> Option { match &self { Value::Map(m) => Some(m.clone()), Value::List(v, ..) if v.is_empty() => Some(SassMap::new()), Value::ArgList(v) if v.is_empty() => Some(SassMap::new()), _ => None, } } pub fn bool(b: bool) -> Self { if b { Value::True } else { Value::False } } pub fn cmp(&self, other: &Self, span: Span, op: BinaryOp) -> SassResult> { Ok(match self { Value::Dimension(SassNumber { num, unit, .. }) => match &other { Value::Dimension(SassNumber { num: num2, unit: unit2, .. }) => { if !unit.comparable(unit2) { return Err( (format!("Incompatible units {} and {}.", unit2, unit), span).into(), ); } if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None { num.partial_cmp(num2) } else { num.partial_cmp(&num2.convert(unit2, unit)) } } _ => { return Err(( format!( "Undefined operation \"{} {} {}\".", self.inspect(span)?, op, other.inspect(span)? ), span, ) .into()) } }, _ => { return Err(( format!( "Undefined operation \"{} {} {}\".", self.inspect(span)?, op, other.inspect(span)? ), span, ) .into()); } }) } pub fn not_equals(&self, other: &Self) -> bool { match self { Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 != s2, _ => true, }, Value::Dimension(SassNumber { num: n, unit, as_slash: _, }) if !n.is_nan() => match other { Value::Dimension(SassNumber { num: n2, unit: unit2, as_slash: _, }) if !n2.is_nan() => { if !unit.comparable(unit2) { true } else if unit == unit2 { n != n2 } else if unit == &Unit::None || unit2 == &Unit::None { true } else { n != &n2.convert(unit2, unit) } } _ => true, }, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { true } else { for (a, b) in list1.iter().zip(list2) { if a.not_equals(b) { return true; } } false } } _ => true, }, s => s != other, } } pub fn as_list(self) -> Vec { match self { Value::List(v, ..) => v, Value::Map(m) => m.as_list(), Value::ArgList(v) => v.elems, v => vec![v], } } pub fn separator(&self) -> ListSeparator { match self { Value::List(_, list_separator, _) => *list_separator, Value::Map(..) | Value::ArgList(..) => ListSeparator::Comma, _ => ListSeparator::Space, } } /// Parses `self` as a selector list, in the same manner as the /// `selector-parse()` function. /// /// Returns a `SassError` if `self` isn't a type that can be parsed as a /// selector, or if parsing fails. If `allow_parent` is `true`, this allows /// parent selectors. Otherwise, they're considered parse errors. /// /// `name` is the argument name. It's used for error reporting. pub fn to_selector( self, visitor: &mut Visitor, name: &str, allows_parent: bool, span: Span, ) -> SassResult { let string = match self.clone().selector_string()? { Some(v) => v, None => return Err((format!("${}: {} is not a valid selector: it must be a string,\n a list of strings, or a list of lists of strings.", name, self.inspect(span)?), span).into()), }; Ok(Selector(visitor.parse_selector_from_string( &string, allows_parent, true, span, )?)) } fn selector_string(self) -> SassResult> { Ok(Some(match self { Value::String(text, ..) => text, Value::List(list, sep, ..) if !list.is_empty() => { let mut result = Vec::new(); match sep { ListSeparator::Comma => { for complex in list { if let Value::String(text, ..) = complex { result.push(text); } else if let Value::List( _, ListSeparator::Space | ListSeparator::Undecided, .., ) = complex { result.push(match complex.selector_string()? { Some(v) => v, None => return Ok(None), }); } else { return Ok(None); } } } ListSeparator::Slash => return Ok(None), ListSeparator::Space | ListSeparator::Undecided => { for compound in list { if let Value::String(text, ..) = compound { result.push(text); } else { return Ok(None); } } } } result.join(sep.as_str()) } _ => return Ok(None), })) } pub fn unary_plus(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(match self { Self::Dimension(SassNumber { .. }) => self, Self::Calculation(..) => { return Err(( format!("Undefined operation \"+{}\".", self.inspect(span)?), span, ) .into()) } _ => Self::String( format!( "+{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, ), }) } pub fn unary_neg(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(match self { Self::Calculation(..) => { return Err(( format!("Undefined operation \"-{}\".", self.inspect(span)?), span, ) .into()) } Self::Dimension(SassNumber { num, unit, as_slash, }) => Self::Dimension(SassNumber { num: -num, unit, as_slash, }), _ => Self::String( format!( "-{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, ), }) } pub fn unary_div(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(Self::String( format!( "/{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, )) } pub fn unary_not(self) -> Self { match self { Self::False | Self::Null => Self::True, _ => Self::False, } } } grass-0.13.4/crates/compiler/src/value/number.rs000066400000000000000000000205761465374720000215740ustar00rootroot00000000000000use std::{ convert::From, fmt, mem, ops::{ Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, }, }; use crate::{ error::SassResult, unit::{Unit, UNIT_CONVERSION_TABLE}, }; use codemap::Span; const PRECISION: i32 = 10; fn epsilon() -> f64 { 10.0_f64.powi(-PRECISION - 1) } fn inverse_epsilon() -> f64 { 10.0_f64.powi(PRECISION + 1) } /// Thin wrapper around `f64` providing utility functions and more accurate /// operations -- namely a Sass-compatible modulo #[derive(Clone, Copy, PartialOrd)] #[repr(transparent)] pub struct Number(pub f64); impl PartialEq for Number { fn eq(&self, other: &Self) -> bool { fuzzy_equals(self.0, other.0) } } impl Eq for Number {} pub(crate) fn fuzzy_equals(a: f64, b: f64) -> bool { if a == b { return true; } (a - b).abs() <= epsilon() && (a * inverse_epsilon()).round() == (b * inverse_epsilon()).round() } pub(crate) fn fuzzy_as_int(num: f64) -> Option { if !num.is_finite() { return None; } let rounded = num.round(); if fuzzy_equals(num, rounded) { // todo: this can oveflow Some(rounded as i64) } else { None } } pub(crate) fn fuzzy_round(number: f64) -> f64 { // If the number is within epsilon of X.5, round up (or down for negative // numbers). if number > 0.0 { if fuzzy_less_than(number % 1.0, 0.5) { number.floor() } else { number.ceil() } } else if fuzzy_less_than_or_equals(number % 1.0, 0.5) { number.floor() } else { number.ceil() } } pub(crate) fn fuzzy_less_than(number1: f64, number2: f64) -> bool { number1 < number2 && !fuzzy_equals(number1, number2) } pub(crate) fn fuzzy_less_than_or_equals(number1: f64, number2: f64) -> bool { number1 < number2 || fuzzy_equals(number1, number2) } impl Number { /// This differs from `std::cmp::min` when either value is NaN pub fn min(self, other: Self) -> Self { if self < other { self } else { other } } /// This differs from `std::cmp::max` when either value is NaN pub fn max(self, other: Self) -> Self { if self > other { self } else { other } } pub fn is_positive(self) -> bool { self.0.is_sign_positive() && !self.is_zero() } pub fn is_negative(self) -> bool { self.0.is_sign_negative() && !self.is_zero() } pub fn assert_int(self, span: Span) -> SassResult { match fuzzy_as_int(self.0) { Some(i) => Ok(i), None => Err((format!("{} is not an int.", self.0), span).into()), } } pub fn round(self) -> Self { Self(self.0.round()) } pub fn ceil(self) -> Self { Self(self.0.ceil()) } pub fn floor(self) -> Self { Self(self.0.floor()) } pub fn abs(self) -> Self { Self(self.0.abs()) } pub fn clamp(self, min: f64, max: f64) -> Self { Number(min.max(self.0.min(max))) } pub fn sqrt(self) -> Self { Self(self.0.sqrt()) } pub fn ln(self) -> Self { Self(self.0.ln()) } pub fn log(self, base: Number) -> Self { Self(self.0.log(base.0)) } pub fn pow(self, exponent: Self) -> Self { Self(self.0.powf(exponent.0)) } /// Invariants: `from.comparable(&to)` must be true pub fn convert(self, from: &Unit, to: &Unit) -> Self { if from == &Unit::None || to == &Unit::None || from == to { return self; } debug_assert!(from.comparable(to), "from: {:?}, to: {:?}", from, to); Number(self.0 * UNIT_CONVERSION_TABLE[to][from]) } } macro_rules! inverse_trig_fn( ($name:ident) => { pub fn $name(self) -> Self { Self(self.0.$name().to_degrees()) } } ); /// Trigonometry methods impl Number { inverse_trig_fn!(acos); inverse_trig_fn!(asin); inverse_trig_fn!(atan); } impl Default for Number { fn default() -> Self { Self::zero() } } impl Number { pub const fn one() -> Self { Self(1.0) } pub fn is_one(self) -> bool { fuzzy_equals(self.0, 1.0) } pub const fn zero() -> Self { Self(0.0) } pub fn is_zero(self) -> bool { fuzzy_equals(self.0, 0.0) } } impl Deref for Number { type Target = f64; fn deref(&self) -> &Self::Target { &self.0 } } macro_rules! from_integer { ($ty:ty) => { impl From<$ty> for Number { fn from(b: $ty) -> Self { Number(b as f64) } } }; } macro_rules! from_smaller_integer { ($ty:ty) => { impl From<$ty> for Number { fn from(val: $ty) -> Self { Self(f64::from(val)) } } }; } impl From for Number { fn from(val: i64) -> Self { Self(val as f64) } } impl From for Number { fn from(b: f64) -> Self { Self(b) } } from_integer!(usize); from_integer!(isize); from_smaller_integer!(i32); from_smaller_integer!(u32); from_smaller_integer!(u8); impl fmt::Debug for Number { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Number( {} )", self.to_string(false)) } } impl Number { pub(crate) fn inspect(self) -> String { self.to_string(false) } pub(crate) fn to_string(self, is_compressed: bool) -> String { if self.0.is_infinite() && self.0.is_sign_negative() { return "-Infinity".to_owned(); } else if self.0.is_infinite() { return "Infinity".to_owned(); } let mut buffer = String::with_capacity(3); if self.0 < 0.0 { buffer.push('-'); } let num = self.0.abs(); if is_compressed && num < 1.0 { buffer.push_str( format!("{:.10}", num)[1..] .trim_end_matches('0') .trim_end_matches('.'), ); } else { buffer.push_str( format!("{:.10}", num) .trim_end_matches('0') .trim_end_matches('.'), ); } if buffer.is_empty() || buffer == "-" || buffer == "-0" { return "0".to_owned(); } buffer } } impl Add for Number { type Output = Self; fn add(self, other: Self) -> Self { Self(self.0 + other.0) } } impl AddAssign for Number { fn add_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp + other; } } impl Sub for Number { type Output = Self; fn sub(self, other: Self) -> Self { Self(self.0 - other.0) } } impl SubAssign for Number { fn sub_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp - other; } } impl Mul for Number { type Output = Self; fn mul(self, other: Self) -> Self { Self(self.0 * other.0) } } impl Mul for Number { type Output = Self; fn mul(self, other: i64) -> Self { Self(self.0 * other as f64) } } impl MulAssign for Number { fn mul_assign(&mut self, other: i64) { let tmp = mem::take(self); *self = tmp * other; } } impl MulAssign for Number { fn mul_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp * other; } } impl Div for Number { type Output = Self; fn div(self, other: Self) -> Self { Self(self.0 / other.0) } } impl DivAssign for Number { fn div_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp / other; } } fn real_mod(n1: f64, n2: f64) -> f64 { n1.rem_euclid(n2) } fn modulo(n1: f64, n2: f64) -> f64 { if n2 > 0.0 { return real_mod(n1, n2); } if n2 == 0.0 { return f64::NAN; } let result = real_mod(n1, n2); if result == 0.0 { 0.0 } else { result + n2 } } impl Rem for Number { type Output = Self; fn rem(self, other: Self) -> Self { Self(modulo(self.0, other.0)) } } impl RemAssign for Number { fn rem_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp % other; } } impl Neg for Number { type Output = Self; fn neg(self) -> Self { Self(-self.0) } } grass-0.13.4/crates/compiler/src/value/sass_function.rs000066400000000000000000000037161465374720000231570ustar00rootroot00000000000000use std::{fmt, sync::Arc}; use crate::{ast::AstFunctionDecl, builtin::Builtin, common::Identifier, evaluate::Environment}; /// A Sass function /// /// The function name is stored in addition to the body /// for use in the builtin function `inspect()` #[derive(Clone, Eq, PartialEq)] pub enum SassFunction { // todo: Cow<'static>? /// Builtin functions are those that have been implemented in Rust and are /// in the global scope. Builtin(Builtin, Identifier), // todo: maybe arc? /// User-defined functions are those that have been implemented in Sass using /// the @function rule. UserDefined(UserDefinedFunction), Plain { name: Identifier, }, } #[derive(Debug, Clone)] pub struct UserDefinedFunction { pub(crate) function: Arc, pub name: Identifier, pub(crate) env: Environment, } impl PartialEq for UserDefinedFunction { fn eq(&self, other: &Self) -> bool { self.function == other.function && self.name == other.name } } impl Eq for UserDefinedFunction {} impl SassFunction { /// Get the name of the function referenced /// /// Used mainly in debugging and `inspect()` pub fn name(&self) -> Identifier { match self { Self::Builtin(_, name) | Self::UserDefined(UserDefinedFunction { name, .. }) | Self::Plain { name } => *name, } } /// Whether the function is builtin or user-defined /// /// Used only in `std::fmt::Debug` for `SassFunction` fn kind(&self) -> &'static str { match &self { Self::Plain { .. } => "Plain", Self::Builtin(..) => "Builtin", Self::UserDefined { .. } => "UserDefined", } } } impl fmt::Debug for SassFunction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SassFunction") .field("name", &self.name()) .field("kind", &self.kind()) .finish() } } grass-0.13.4/crates/compiler/src/value/sass_number.rs000066400000000000000000000234221465374720000226160ustar00rootroot00000000000000use std::{ ops::{Add, Div, Mul, Sub}, sync::Arc, }; use codemap::Span; use crate::{ error::SassResult, serializer::{inspect_float, inspect_number}, unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE}, Options, }; use super::{fuzzy_as_int, Number}; #[derive(Debug, Clone)] pub struct SassNumber { pub num: Number, pub unit: Unit, pub as_slash: Option>, } pub(crate) fn conversion_factor(from: &Unit, to: &Unit) -> Option { if from == to { return Some(1.0); } UNIT_CONVERSION_TABLE.get(to)?.get(from).copied() } impl SassNumber { pub fn new_unitless>(n: N) -> Self { Self { num: n.into(), unit: Unit::None, as_slash: None, } } pub fn has_comparable_units(&self, other_unit: &Unit) -> bool { self.unit.comparable(other_unit) } /// Unlike [`SassNumber::has_comparable_units`], this considers `Unit::None` /// to be compatible only with itself pub fn has_compatible_units(&self, other_unit: &Unit) -> bool { if (self.unit == Unit::None || *other_unit == Unit::None) && self.unit != *other_unit { return false; } self.has_comparable_units(other_unit) } #[allow(clippy::collapsible_if)] pub(crate) fn multiply_units(&self, mut num: f64, other_unit: Unit) -> SassNumber { let (numer_units, denom_units) = self.unit.clone().numer_and_denom(); let (other_numer, other_denom) = other_unit.numer_and_denom(); if numer_units.is_empty() { if other_denom.is_empty() && !are_any_convertible(&denom_units, &other_numer) { return SassNumber { num: Number(num), unit: Unit::new(other_numer, denom_units), as_slash: None, }; } else if denom_units.is_empty() { return SassNumber { num: Number(num), unit: Unit::new(other_numer, other_denom), as_slash: None, }; } } else if other_numer.is_empty() { if other_denom.is_empty() || (denom_units.is_empty() && !are_any_convertible(&numer_units, &other_denom)) { return SassNumber { num: Number(num), unit: Unit::new(numer_units, other_denom), as_slash: None, }; } } let mut new_numer = Vec::new(); let mut mutable_other_denom = other_denom; for numer in numer_units { let mut has_removed = false; mutable_other_denom.retain(|denom| { if has_removed { return true; } if let Some(factor) = conversion_factor(denom, &numer) { num /= factor; has_removed = true; return false; } true }); if !has_removed { new_numer.push(numer); } } let mut mutable_denom = denom_units; for numer in other_numer { let mut has_removed = false; mutable_denom.retain(|denom| { if has_removed { return true; } if let Some(factor) = conversion_factor(denom, &numer) { num /= factor; has_removed = true; return false; } true }); if !has_removed { new_numer.push(numer); } } mutable_denom.append(&mut mutable_other_denom); SassNumber { num: Number(num), unit: Unit::new(new_numer, mutable_denom), as_slash: None, } } pub fn assert_no_units(&self, name: &str, span: Span) -> SassResult<()> { if self.unit == Unit::None { Ok(()) } else { Err(( format!( "${name}: Expected {} to have no units.", inspect_number(self, &Options::default(), span)?, name = name, ), span, ) .into()) } } pub fn assert_unit(&self, unit: &Unit, name: &str, span: Span) -> SassResult<()> { if self.unit == *unit { Ok(()) } else { Err(( format!( "${name}: Expected {} to have unit \"{unit}\".", inspect_number(self, &Options::default(), span)?, name = name, unit = unit, ), span, ) .into()) } } pub fn assert_bounds(&self, name: &str, min: f64, max: f64, span: Span) -> SassResult<()> { self.assert_bounds_with_unit(name, min, max, &self.unit, span) } pub fn assert_int_with_name(&self, name: &'static str, span: Span) -> SassResult { match fuzzy_as_int(self.num.0) { Some(i) => Ok(i), None => Err(( format!( "${name}: {} is not an int.", inspect_number(self, &Options::default(), span)?, name = name, ), span, ) .into()), } } pub fn assert_bounds_with_unit( &self, name: &str, min: f64, max: f64, unit: &Unit, span: Span, ) -> SassResult<()> { if !(self.num <= Number(max) && self.num >= Number(min)) { return Err(( format!( "${}: Expected {} to be within {}{} and {}{}.", name, inspect_number(self, &Options::default(), span)?, inspect_float(min, &Options::default(), span), unit, inspect_float(max, &Options::default(), span), unit, ), span, ) .into()); } Ok(()) } pub fn is_comparable_to(&self, other: &Self) -> bool { self.unit.comparable(&other.unit) } /// For use in calculations pub fn has_possibly_compatible_units(&self, other: &Self) -> bool { if self.unit.is_complex() || other.unit.is_complex() { return false; } let known_compatibilities = match known_compatibilities_by_unit(&self.unit) { Some(known_compatibilities) => known_compatibilities, None => return true, }; known_compatibilities.contains(&other.unit) || known_compatibilities_by_unit(&other.unit).is_none() } pub fn unit(&self) -> &Unit { &self.unit } } impl PartialEq for SassNumber { fn eq(&self, other: &Self) -> bool { if !self.unit.comparable(&other.unit) { return false; } if (other.unit == Unit::None || self.unit == Unit::None) && self.unit != other.unit { return false; } self.num == other.num.convert(&other.unit, &self.unit) } } impl Eq for SassNumber {} impl Add for SassNumber { type Output = SassNumber; fn add(self, rhs: SassNumber) -> Self::Output { if self.unit == rhs.unit { SassNumber { num: self.num + rhs.num, unit: self.unit, as_slash: None, } } else if self.unit == Unit::None { SassNumber { num: self.num + rhs.num, unit: rhs.unit, as_slash: None, } } else if rhs.unit == Unit::None { SassNumber { num: self.num + rhs.num, unit: self.unit, as_slash: None, } } else { SassNumber { num: self.num + rhs.num.convert(&rhs.unit, &self.unit), unit: self.unit, as_slash: None, } } } } impl Sub for SassNumber { type Output = SassNumber; fn sub(self, rhs: SassNumber) -> Self::Output { if self.unit == rhs.unit { SassNumber { num: self.num - rhs.num, unit: self.unit, as_slash: None, } } else if self.unit == Unit::None { SassNumber { num: self.num - rhs.num, unit: rhs.unit, as_slash: None, } } else if rhs.unit == Unit::None { SassNumber { num: self.num - rhs.num, unit: self.unit, as_slash: None, } } else { SassNumber { num: self.num - rhs.num.convert(&rhs.unit, &self.unit), unit: self.unit, as_slash: None, } } } } impl Mul for SassNumber { type Output = SassNumber; fn mul(self, rhs: SassNumber) -> Self::Output { if rhs.unit == Unit::None { return SassNumber { num: self.num * rhs.num, unit: self.unit, as_slash: None, }; } self.multiply_units(self.num.0 * rhs.num.0, rhs.unit) } } impl Div for SassNumber { type Output = SassNumber; fn div(self, rhs: SassNumber) -> Self::Output { if rhs.unit == Unit::None { return SassNumber { num: self.num / rhs.num, unit: self.unit, as_slash: None, }; } self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert()) } } grass-0.13.4/crates/include_sass/000077500000000000000000000000001465374720000166635ustar00rootroot00000000000000grass-0.13.4/crates/include_sass/Cargo.toml000066400000000000000000000013211465374720000206100ustar00rootroot00000000000000[package] name = "include_sass" version = "0.13.4" edition = "2021" description = "Internal implementation of the grass::include! macro" readme = "../../README.md" license = "MIT" categories = ["web-programming"] keywords = ["scss", "sass", "css", "web"] repository = "https://github.com/connorskees/grass" authors = ["Connor Skees <39542938+ConnorSkees@users.noreply.github.com>"] include = ["src", "Cargo.toml", "../README.md", "../CHANGELOG.md", "../LICENSE"] rust-version = "1.70" [lib] proc-macro = true [dependencies] syn = { version = "2", default-features = false } grass_compiler = { path = "../compiler", version = "=0.13.4" } quote = { version = "1.0.23", default-features = false } [features] nightly = [] grass-0.13.4/crates/include_sass/src/000077500000000000000000000000001465374720000174525ustar00rootroot00000000000000grass-0.13.4/crates/include_sass/src/lib.rs000066400000000000000000000055621465374720000205760ustar00rootroot00000000000000#![cfg_attr(feature = "nightly", feature(track_path))] use std::{cell::RefCell, collections::HashSet, path::PathBuf}; use grass_compiler::StdFs; use proc_macro::TokenStream; #[cfg(not(feature = "nightly"))] use quote::format_ident; use syn::{parse_macro_input, LitStr}; use quote::__private::TokenStream as TokenStream2; #[derive(Debug)] struct FileTracker<'a> { files: RefCell>, fs: &'a dyn grass_compiler::Fs, } impl<'a> grass_compiler::Fs for FileTracker<'a> { fn is_dir(&self, path: &std::path::Path) -> bool { #[cfg(feature = "nightly")] if let Ok(p) = std::fs::canonicalize(path) { self.files.borrow_mut().insert(p); } self.fs.is_dir(path) } fn is_file(&self, path: &std::path::Path) -> bool { #[cfg(feature = "nightly")] if let Ok(p) = std::fs::canonicalize(path) { self.files.borrow_mut().insert(p); } self.fs.is_file(path) } fn read(&self, path: &std::path::Path) -> std::io::Result> { if let Ok(p) = std::fs::canonicalize(path) { self.files.borrow_mut().insert(p); } self.fs.read(path) } } #[cfg(not(feature = "nightly"))] fn track_files(files: &HashSet) -> TokenStream2 { let mut s: TokenStream2 = quote::quote!(); for (idx, file) in files.iter().enumerate() { let ident = format_ident!("__VAR{}", idx); let file_name = file.to_string_lossy(); s.extend::(quote::quote!( const #ident: &str = include_str!(#file_name); )); } s } #[cfg(feature = "nightly")] fn track_files(files: &HashSet) { for file in files { proc_macro::tracked_path::path(file.to_string_lossy()); } } #[cfg(not(feature = "nightly"))] fn finish(css: String, files: &HashSet) -> TokenStream { let files = track_files(files); quote::quote!( { #files #css } ) .into() } #[cfg(feature = "nightly")] fn finish(css: String, files: &HashSet) -> TokenStream { track_files(files); quote::quote!(#css).into() } #[proc_macro] pub fn include_sass(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as LitStr); let options = grass_compiler::Options::default(); let fs = FileTracker { files: RefCell::new(HashSet::new()), fs: &StdFs, }; let value = input.value(); let css = match grass_compiler::from_path( value, &options .fs(&fs) .style(grass_compiler::OutputStyle::Compressed), ) { Ok(css) => css, Err(e) => { let err = syn::Error::new(input.span(), format!("Failed to compile Sass\n{}", e)); return syn::Error::into_compile_error(err).into(); } }; let files = &*fs.files.borrow(); finish(css, files) } grass-0.13.4/crates/lib/000077500000000000000000000000001465374720000147555ustar00rootroot00000000000000grass-0.13.4/crates/lib/Cargo.toml000066400000000000000000000031771465374720000167150ustar00rootroot00000000000000[package] name = "grass" version = "0.13.4" description = "A Sass compiler written purely in Rust" readme = "../../README.md" license = "MIT" categories = ["command-line-utilities", "web-programming"] keywords = ["scss", "sass", "css", "web"] repository = "https://github.com/connorskees/grass" authors = ["Connor Skees <39542938+ConnorSkees@users.noreply.github.com>"] edition = "2021" include = [ "src", "Cargo.toml", "README.md", "CHANGELOG.md", "Cargo.lock", "LICENSE", ] default-run = "grass" rust-version = "1.70" [[bin]] name = "grass" path = "src/main.rs" required-features = ["commandline"] [lib] name = "grass" path = "src/lib.rs" crate-type = ["cdylib", "rlib"] bench = false [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg doc_cfg" cargo +nightly doc --features=macro --no-deps --open features = ["macro"] rustdoc-args = ["--cfg", "doc_cfg"] [dependencies] wasm-bindgen = { version = "0.2", optional = true } getrandom = { version = "0.2", features = ["js"] } grass_compiler = { path = "../compiler", version = "=0.13.4", default-features = false } include_sass = { path = "../include_sass", version = "0.13.4", optional = true } clap = { version = "4.3.10", optional = true } [features] # todo: no commandline by default default = ["commandline", "random"] # Option (enabled by default): build a binary using clap commandline = ["clap"] random = ["grass_compiler/random"] wasm-exports = ["grass_compiler/wasm-exports", "wasm-bindgen"] # Option: include the proc macro `include_sass!` macro = ["include_sass"] nightly = ["include_sass/nightly"] [dev-dependencies] tempfile = "3.3.0" paste = "1.0.3" grass-0.13.4/crates/lib/src/000077500000000000000000000000001465374720000155445ustar00rootroot00000000000000grass-0.13.4/crates/lib/src/lib.rs000066400000000000000000000063641465374720000166710ustar00rootroot00000000000000/*! This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS. This crate targets compatibility with the reference implementation in Dart. If upgrading from the [now deprecated](https://sass-lang.com/blog/libsass-is-deprecated) `libsass`, one may have to modify their stylesheets. These changes will not differ from those necessary to upgrade to `dart-sass`, and in general such changes should be quite rare. This crate is capable of compiling Bootstrap 4 and 5, bulma and bulma-scss, Bourbon, as well as most other large Sass libraries with complete accuracy. For the vast majority of use cases there should be no perceptible differences from the reference implementation. ## Use as library ``` fn main() -> Result<(), Box> { let css = grass::from_string( "a { b { color: &; } }".to_owned(), &grass::Options::default() )?; assert_eq!(css, "a b {\n color: a b;\n}\n"); Ok(()) } ``` ## Use as binary ```bash cargo install grass grass input.scss ``` */ #![cfg_attr(doc_cfg, feature(doc_cfg))] #![warn(clippy::all, clippy::cargo, clippy::dbg_macro)] #![deny(missing_debug_implementations)] #![allow( clippy::use_self, // filter isn't fallible clippy::manual_filter_map, renamed_and_removed_lints, clippy::unknown_clippy_lints, clippy::single_match, clippy::new_without_default, clippy::single_match_else, clippy::multiple_crate_versions, clippy::wrong_self_convention, clippy::comparison_chain, // these features are only available on nightly clippy::unnested_or_patterns, clippy::uninlined_format_args, // todo: these should be enabled clippy::cast_sign_loss, clippy::cast_lossless, clippy::cast_precision_loss, clippy::float_cmp, // todo: unignore once we bump MSRV clippy::format_push_string, clippy::unnecessary_unwrap, clippy::needless_late_init, unknown_lints, )] pub use grass_compiler::{ from_path, from_string, Error, ErrorKind, Fs, InputSyntax, Logger, NullFs, NullLogger, Options, OutputStyle, Result, StdFs, StdLogger, }; /// Include CSS in your binary at compile time from a Sass source file /// /// `static CSS: &str = grass::include!("../static/_index.scss");` /// /// This requires the `"macro"` feature, which is not enabled by default. /// /// By default `grass` will track files using [`include_str!`]. This allows incremental /// compilation to be updated when any Sass files are modified. /// /// If compiling with a nightly version of rust, `grass` can make use of /// [proc_macro::tracked_path](https://github.com/rust-lang/rust/issues/99515) /// in order to force incremental recompilation, which is more robust and potentially /// faster. This is enabled by the `"nightly"` feature. /// /// ###### Limitations /// /// Compilation options are not configurable with this macro. The default values /// for all options are used, except for output style, which is compressed. #[macro_export] #[cfg(any(feature = "macro", doc))] #[cfg_attr(doc_cfg, doc(cfg(feature = "macro")))] macro_rules! include { ($path:literal) => { $crate::__internal::include_sass::include_sass!($path); }; } #[doc(hidden)] #[cfg(feature = "macro")] pub mod __internal { #[doc(hidden)] pub use include_sass; } grass-0.13.4/crates/lib/src/main.rs000066400000000000000000000202101465374720000170310ustar00rootroot00000000000000use std::{ fs::OpenOptions, io::{stdin, stdout, Read, Write}, path::Path, }; use clap::{builder::PossibleValue, value_parser, Arg, ArgAction, Command, ValueEnum}; use grass::{from_path, from_string, Options, OutputStyle}; #[derive(Eq, PartialEq, Debug, Clone, Copy)] pub enum Style { Expanded, Compressed, } impl ValueEnum for Style { fn value_variants<'a>() -> &'a [Self] { &[Self::Expanded, Self::Compressed] } fn to_possible_value(&self) -> Option { Some(match self { Self::Expanded => PossibleValue::new("expanded"), Self::Compressed => PossibleValue::new("compressed"), }) } } #[derive(Eq, PartialEq, Debug, Clone)] pub enum SourceMapUrls { Relative, Absolute, } impl ValueEnum for SourceMapUrls { fn value_variants<'a>() -> &'a [Self] { &[Self::Relative, Self::Absolute] } fn to_possible_value(&self) -> Option { Some(match self { Self::Relative => PossibleValue::new("relative"), Self::Absolute => PossibleValue::new("absolute"), }) } } fn cli() -> Command { Command::new("grass") .version(env!("CARGO_PKG_VERSION")) .about("A Sass compiler written purely in Rust") .disable_version_flag(true) .arg( Arg::new("version") .action(ArgAction::Version) .long("version") .short('v') .global(true) ) .arg( Arg::new("STDIN") .action(ArgAction::SetTrue) .long("stdin") .help("Read the stylesheet from stdin"), ) .arg( Arg::new("INDENTED") .long("indented") .hide(true) .help("Use the indented syntax for input from stdin"), ) .arg( Arg::new("LOAD_PATH") .short('I') .long("load-path") .help("A path to use when resolving imports. May be passed multiple times.") .action(ArgAction::Append) .value_parser(value_parser!(String)) .num_args(1) ) .arg( Arg::new("STYLE") // this is required for compatibility with ruby sass .short_alias('t') .short('s') .long("style") .help("Minified or expanded output") .default_value("expanded") .ignore_case(true) .num_args(1) .value_parser(value_parser!(Style)), ) .arg( Arg::new("NO_CHARSET") .action(ArgAction::SetTrue) .long("no-charset") .help("Don't emit a @charset or BOM for CSS with non-ASCII characters."), ) .arg( Arg::new("UPDATE") .long("update") .hide(true) .help("Only compile out-of-date stylesheets."), ) .arg( Arg::new("NO_ERROR_CSS") .long("no-error-css") .hide(true) .help("When an error occurs, don't emit a stylesheet describing it."), ) // Source maps .arg( Arg::new("NO_SOURCE_MAP") .long("no-source-map") .hide(true) .help("Whether to generate source maps."), ) .arg( Arg::new("SOURCE_MAP_URLS") .long("source-map-urls") .hide(true) .help("How to link from source maps to source files.") .default_value("relative") .ignore_case(true) .num_args(1) .value_parser(value_parser!(SourceMapUrls)), ) .arg( Arg::new("EMBED_SOURCES") .long("embed-sources") .hide(true) .help("Embed source file contents in source maps."), ) .arg( Arg::new("EMBED_SOURCE_MAP") .long("embed-source-map") .hide(true) .help("Embed source map contents in CSS."), ) // Other .arg( Arg::new("WATCH") .long("watch") .hide(true) .help("Watch stylesheets and recompile when they change."), ) .arg( Arg::new("POLL") .long("poll") .hide(true) .help("Manually check for changes rather than using a native watcher. Only valid with --watch.") .requires("WATCH"), ) .arg( Arg::new("NO_STOP_ON_ERROR") .long("no-stop-on-error") .hide(true) .help("Continue to compile more files after error is encountered.") ) .arg( Arg::new("INTERACTIVE") .short('i') .long("interactive") .hide(true) .help("Run an interactive SassScript shell.") ) .arg( Arg::new("NO_COLOR") .short('c') .action(ArgAction::SetTrue) .long("no-color") .hide(true) .help("Whether to use terminal colors for messages.") ) .arg( Arg::new("VERBOSE") .action(ArgAction::SetTrue) .long("verbose") .hide(true) .help("Print all deprecation warnings even when they're repetitive.") ) .arg( Arg::new("NO_UNICODE") .action(ArgAction::SetTrue) .long("no-unicode") .help("Whether to use Unicode characters for messages.") ) .arg( Arg::new("QUIET") .action(ArgAction::SetTrue) .short('q') .long("quiet") .help("Don't print warnings."), ) .arg( Arg::new("INPUT") .value_parser(value_parser!(String)) .required_unless_present("STDIN") .help("Sass files"), ) .arg( Arg::new("OUTPUT") .help("Output CSS file") ) // Hidden, legacy arguments .arg( Arg::new("PRECISION") .long("precision") .hide(true) .num_args(1) ) } fn main() -> std::io::Result<()> { let matches = cli().get_matches(); let load_paths = matches .get_many::("LOAD_PATH") .map_or_else(Vec::new, |vals| vals.map(Path::new).collect()); let style = match &matches.get_one::