diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml
new file mode 100644
index 000000000..ad2abda5e
--- /dev/null
+++ b/.github/util/initialize/action.yml
@@ -0,0 +1,36 @@
+name: Initialize
+description: Check out Dart Sass and build the embedded protocol buffer.
+inputs:
+ github-token: {required: true}
+ node-version: {required: false, default: 18}
+ dart-sdk: {required: false, default: stable}
+ architecture: {required: false}
+runs:
+ using: composite
+ steps:
+ - uses: dart-lang/setup-dart@v1
+ with:
+ sdk: "${{ inputs.dart-sdk }}"
+ architecture: "${{ inputs.architecture }}"
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: "${{ inputs.node-version }}"
+
+ - run: dart pub get
+ shell: bash
+
+ - run: npm install
+ shell: bash
+
+ - uses: bufbuild/buf-setup-action@v1.13.1
+ with: {github_token: "${{ inputs.github-token }}"}
+
+ - name: Check out the language repo
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/sass, path: build/language}
+
+ - name: Generate Dart from protobuf
+ run: dart run grinder protobuf
+ env: {UPDATE_SASS_PROTOCOL: false}
+ shell: bash
diff --git a/.github/util/sass-spec/action.yml b/.github/util/sass-spec/action.yml
new file mode 100644
index 000000000..d44b25db8
--- /dev/null
+++ b/.github/util/sass-spec/action.yml
@@ -0,0 +1,12 @@
+name: sass-spec
+description: Check out sass-spec and install its dependencies.
+runs:
+ using: composite
+ steps:
+ - name: Check out sass-spec
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/sass-spec}
+
+ - run: npm install
+ working-directory: sass-spec
+ shell: bash
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ec0a3702b..2f8fdc4f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,11 +3,11 @@ name: CI
defaults:
run: {shell: bash}
-env:
- # Note: when changing this, also change
- # jobs.node_tests.strategy.matrix.node_version and the Node version for Dart
- # dev tests.
- DEFAULT_NODE_VERSION: 16
+# The default Node version lives in ../util/initialize/action.yml. It should be
+# kept up-to-date with the latest Node LTS releases, along with the various
+# node-version matrices below.
+#
+# Next update: April 2021
on:
push:
@@ -16,6 +16,46 @@ on:
pull_request:
jobs:
+ format:
+ name: Code formatting
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dart-lang/setup-dart@v1
+ - run: dart format --fix .
+ - run: git diff --exit-code
+
+ static_analysis:
+ name: Static analysis
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Analyze Dart
+ run: dart analyze --fatal-warnings ./
+
+ dartdoc:
+ name: Dartdoc
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: dartdoc sass
+ run: dart run dartdoc --quiet --no-generate-docs
+ --errors ambiguous-doc-reference,broken-link,deprecated
+ --errors unknown-directive,unknown-macro,unresolved-doc-reference
+ - name: dartdoc sass_api
+ run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
+ --errors ambiguous-doc-reference,broken-link,deprecated
+ --errors unknown-directive,unknown-macro,unresolved-doc-reference
+
sass_spec_language:
name: "Language Tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.async_label }}"
runs-on: ubuntu-latest
@@ -33,16 +73,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- with: {sdk: "${{ matrix.dart_channel }}"}
- - run: dart pub get
- - name: Check out sass-spec
- uses: sass/clone-linked-repo@v1
- with: {repo: sass/sass-spec}
- - uses: actions/setup-node@v3
- with: {node-version: "${{ env.DEFAULT_NODE_VERSION }}"}
- - run: npm install
- working-directory: sass-spec
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ - uses: ./.github/util/sass-spec
+
- name: Run specs
run: npm run sass-spec -- --dart .. $extra_args
working-directory: sass-spec
@@ -52,7 +88,7 @@ jobs:
# They next need to be rotated April 2021. See
# https://github.com/nodejs/Release.
sass_spec_js:
- name: "JS API Tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node_version }} | ${{ matrix.os }}"
+ name: "JS API Tests | Pure JS | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
runs-on: "${{ matrix.os }}"
strategy:
@@ -60,39 +96,122 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dart_channel: [stable]
- node_version: [16]
+ node-version: [18]
include:
# Include LTS versions on Ubuntu
- os: ubuntu-latest
dart_channel: stable
- node_version: 14
+ node-version: 16
- os: ubuntu-latest
dart_channel: stable
- node_version: 12
+ node-version: 14
- os: ubuntu-latest
dart_channel: dev
- node_version: 16
+ node-version: 18
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- with: {sdk: "${{ matrix.dart_channel }}"}
- - run: dart pub get
- - uses: actions/setup-node@v3
- with: {node-version: "${{ matrix.node_version }}"}
- - run: npm install
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+ - uses: ./.github/util/sass-spec
- - name: Check out sass-spec
+ - name: Build JS
+ run: dart run grinder pkg-npm-dev
+
+ - name: Check out Sass specification
uses: sass/clone-linked-repo@v1
- with: {repo: sass/sass-spec}
+ with:
+ repo: sass/sass
+ path: language
- - name: Install sass-spec dependencies
- run: npm install
+ - name: Run tests
+ run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm
working-directory: sass-spec
+ # The versions should be kept up-to-date with the latest LTS Node releases.
+ # They next need to be rotated October 2021. See
+ # https://github.com/nodejs/Release.
+ sass_spec_js_embedded:
+ name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}'
+ runs-on: ${{ matrix.os }}-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu, windows, macos]
+ node-version: [18]
+ include:
+ # Include LTS versions on Ubuntu
+ - os: ubuntu
+ node-version: 16
+ - os: ubuntu
+ node-version: 14
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/util/initialize
+ with:
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+ - uses: ./.github/util/sass-spec
+
+ - name: Check out the embedded host
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/embedded-host-node}
+
+ - name: Check out the language repo
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/sass, path: build/language}
+
+ - name: Initialize embedded host
+ run: |
+ npm install
+ npm run init -- --compiler-path=.. --language-path=../build/language
+ npm run compile
+ mv {`pwd`/,dist/}lib/src/vendor/dart-sass
+ working-directory: embedded-host-node
+
+ - name: Version info
+ run: |
+ path=embedded-host-node/dist/lib/src/vendor/dart-sass/sass
+ if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version
+ elif [[ -f "$path.bat" ]]; then "./$path.bat" --version
+ elif [[ -f "$path.exe" ]]; then "./$path.exe" --version
+ else "./$path" --version
+ fi
+
+ - name: Run tests
+ run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../build/language
+ working-directory: sass-spec
+
+ sass_spec_js_browser:
+ name: "JS API Tests | Browser | Dart ${{ matrix.dart_channel }}"
+
+ strategy:
+ matrix:
+ dart_channel: [stable]
+ fail-fast: false
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: browser-actions/setup-chrome@v1
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ - uses: ./.github/util/sass-spec
+
- name: Build JS
run: dart run grinder pkg-npm-dev
+ - name: Install built dependencies
+ run: npm install
+ working-directory: build/npm
+
- name: Check out Sass specification
uses: sass/clone-linked-repo@v1
with:
@@ -100,8 +219,10 @@ jobs:
path: language
- name: Run tests
- run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm
+ run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm --browser
working-directory: sass-spec
+ env:
+ CHROME_EXECUTABLE: chrome
dart_tests:
name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}"
@@ -116,12 +237,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- with: {sdk: "${{ matrix.dart_channel }}"}
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+
- run: dart run grinder pkg-standalone-dev
- name: Run tests
- run: dart run test -p vm -x node -r expanded
+ run: dart run test -x node
# Unit tests that use Node.js, defined in test/.
#
@@ -129,7 +252,7 @@ jobs:
# They next need to be rotated April 2021. See
# https://github.com/nodejs/Release.
node_tests:
- name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node_version }} | ${{ matrix.os }}"
+ name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
runs-on: "${{ matrix.os }}"
strategy:
@@ -138,58 +261,52 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dart_channel: [stable]
- node_version: [16]
+ node-version: [18]
include:
# Include LTS versions on Ubuntu
- os: ubuntu-latest
dart_channel: stable
- node_version: 14
+ node-version: 16
- os: ubuntu-latest
dart_channel: stable
- node_version: 12
+ node-version: 14
- os: ubuntu-latest
dart_channel: dev
- node_version: 16
-
+ node-version: 18
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- with: {sdk: "${{ matrix.dart_channel }}"}
- - run: dart pub get
- - uses: actions/setup-node@v3
- with: {node-version: "${{ matrix.node_version }}"}
- - run: npm install
- - run: dart run grinder before-test
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+
+ - run: dart run grinder pkg-npm-dev
- name: Run tests
- run: dart run test -j 2 -t node -r expanded
+ run: dart run test -t node -j 2
- static_analysis:
- name: Static analysis
- runs-on: ubuntu-latest
+ browser_tests:
+ name: "Browser Tests | Dart ${{ matrix.dart_channel }}"
- steps:
- - uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
- - name: Analyze Dart
- run: dart analyze --fatal-warnings --fatal-infos .
+ strategy:
+ matrix:
+ dart_channel: [stable]
+ fail-fast: false
- dartdoc:
- name: Dartdoc
runs-on: ubuntu-latest
-
steps:
- - uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
- - name: dartdoc sass
- run: dart run dartdoc --quiet --no-generate-docs
- --errors ambiguous-doc-reference,broken-link,deprecated
- --errors unknown-directive,unknown-macro,unresolved-doc-reference
- - name: dartdoc sass_api
- run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
- --errors ambiguous-doc-reference,broken-link,deprecated
- --errors unknown-directive,unknown-macro,unresolved-doc-reference
+ - uses: actions/checkout@v3
+ - uses: browser-actions/setup-chrome@v1
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+
+ - run: dart run grinder pkg-npm-dev
+ - name: Run tests
+ run: dart run test -p chrome -j 2
+ env:
+ CHROME_EXECUTABLE: chrome
double_check:
name: Double-check
@@ -197,16 +314,21 @@ jobs:
needs:
- sass_spec_language
- sass_spec_js
+ - sass_spec_js_browser
+ - sass_spec_js_embedded
- dart_tests
- node_tests
+ - browser_tests
- static_analysis
- dartdoc
+ - format
if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Run checks
run: dart run grinder double-check-before-release
@@ -222,8 +344,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- run: dart run grinder fetch-bootstrap${{matrix.bootstrap_version}}
env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- name: Build
@@ -236,8 +359,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- run: dart run grinder fetch-bourbon
env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- name: Test
@@ -252,8 +376,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- run: dart run grinder fetch-foundation
env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
# TODO(nweiz): Foundation has proper Sass tests, but they're currently not
@@ -269,8 +394,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- run: dart run grinder fetch-bulma
env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- name: Build
@@ -284,8 +410,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
run: dart run grinder pkg-github-release pkg-github-linux-ia32 pkg-github-linux-x64
env:
@@ -307,6 +434,9 @@ jobs:
steps:
- uses: actions/checkout@v3
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- uses: docker/setup-qemu-action@v2
- name: Deploy
run: |
@@ -342,11 +472,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
+ - uses: ./.github/util/initialize
# Workaround for dart-lang/setup-dart#59
with:
+ github-token: ${{ github.token }}
architecture: ${{ matrix.architecture }}
- - run: dart pub get
+
- name: Deploy
run: dart run grinder pkg-github-${{ matrix.platform }}
env:
@@ -361,10 +492,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
- - uses: actions/setup-node@v3
- with: {node-version: "${{ env.DEFAULT_NODE_VERSION }}"}
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
run: dart run grinder pkg-npm-deploy
env:
@@ -378,10 +508,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
- - uses: actions/setup-node@v3
- with: {node-version: "${{ env.DEFAULT_NODE_VERSION }}"}
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
run: dart run grinder update-bazel
env:
@@ -396,12 +525,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
- - uses: actions/setup-node@v3
- with: {node-version: "${{ env.DEFAULT_NODE_VERSION }}"}
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
- run: dart run grinder pkg-pub-deploy
+ run: dart run grinder protobuf pkg-pub-deploy
env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"}
deploy_sub_packages:
@@ -412,8 +540,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
run: dart run grinder deploy-sub-packages
env:
@@ -431,6 +560,7 @@ jobs:
- uses: actions/checkout@v3
- uses: dart-lang/setup-dart@v1
- run: dart pub get
+
- name: Deploy
run: dart run grinder pkg-homebrew-update
env:
@@ -445,8 +575,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
- name: Deploy
run: dart run grinder pkg-chocolatey-deploy
env: {CHOCOLATEY_TOKEN: "${{ secrets.CHOCOLATEY_TOKEN }}"}
@@ -456,66 +587,57 @@ jobs:
runs-on: ubuntu-latest
needs: [bootstrap, bourbon, foundation, bulma]
if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
steps:
- uses: actions/checkout@v3
with:
repository: sass/sass-site
- token: ${{ secrets.GH_TOKEN }}
+ token: ${{ secrets.SASS_SITE_TOKEN }}
- - uses: EndBug/add-and-commit@v8
+ - uses: EndBug/add-and-commit@v9
with:
author_name: Sass Bot
author_email: sass.bot.beep.boop@gmail.com
message: Cut a release for a new Dart Sass version
commit: --allow-empty
- release_embedded_compiler:
- name: "Release Embedded Compiler"
+ release_embedded_host:
+ name: "Release Embedded Host"
runs-on: ubuntu-latest
- needs: [deploy_pub, deploy_sub_packages]
+ needs: [deploy_github_linux, deploy_github_linux_qemu, deploy_github]
if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
steps:
- uses: actions/checkout@v3
with:
- repository: sass/dart-sass-embedded
+ repository: sass/embedded-host-node
token: ${{ secrets.GH_TOKEN }}
- - uses: dart-lang/setup-dart@v1
- - uses: frenck/action-setup-yq@v1
- with: {version: v4.30.5} # frenck/action-setup-yq#35
- name: Get version
id: version
- run: echo "::set-output name=version::${GITHUB_REF##*/}"
+ run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT"
- name: Update version
run: |
- sed -i 's/version: .*/version: ${{ steps.version.outputs.version }}/' pubspec.yaml
- dart pub remove sass
- dart pub add sass:${{ steps.version.outputs.version }}
-
- # Delete a dependency override on Sass if it exists, and delete the
- # dependency_overrides field if it's now empty. The embedded compiler
- # often uses dev dependencies to run against the latest Dart Sass, but
- # once we release the latest version that's no longer necessary.
- #
- # TODO(dart-lang/pub#3700): Use pub for this instead to avoid removing
- # blank lines. See also mikefarah/yq#515.
- yq -i '
- del(.dependency_overrides.sass) |
- to_entries |
- map(select(.key != "dependency_overrides" or (.value | length >0))) |
- from_entries
- ' pubspec.yaml
-
- # The embedded compiler has a checked-in pubspec.yaml, so upgrade to
- # make sure we're releasing against the latest version of all deps.
- dart pub upgrade
-
+ # Update binary package versions
+ for dir in $(ls npm); do
+ cat "npm/$dir/package.json" |
+ jq --arg version ${{ steps.version.outputs.version }} '
+ .version |= $version
+ ' > package.json.tmp &&
+ mv package.json.tmp "npm/$dir/package.json"
+ done
+
+ # Update main package version and dependencies on binary packages
+ cat package.json |
+ jq --arg version ${{ steps.version.outputs.version }} '
+ .version |= $version |
+ ."compiler-version" |= $version |
+ .optionalDependencies = (.optionalDependencies | .[] |= $version)
+ ' > package.json.tmp &&
+ mv package.json.tmp package.json
curl https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md
+ shell: bash
- - uses: EndBug/add-and-commit@v8
+ - uses: EndBug/add-and-commit@v9
with:
author_name: Sass Bot
author_email: sass.bot.beep.boop@gmail.com
diff --git a/.gitignore b/.gitignore
index 89b6acfd6..2c61888e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ package-lock.json
node_modules/
/doc/api
/pkg/*/doc/api
+
+# Generated protocol buffer files.
+*.pb*.dart
diff --git a/.pubignore b/.pubignore
new file mode 100644
index 000000000..2fbab300a
--- /dev/null
+++ b/.pubignore
@@ -0,0 +1,19 @@
+# This should be identical to .gitignore except that it doesn't exclude
+# generated protobuf files.
+
+.buildlog
+.DS_Store
+.idea
+.pub/
+.dart_tool/
+.settings/
+.sass-cache/
+build/
+packages
+.packages
+pubspec.lock
+package-lock.json
+/benchmark/source
+node_modules/
+/doc/api
+/pkg/*/doc/api
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0721fa70..55986797b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,15 @@
-## 1.58.0
+## 1.67.0
+
+* **Breaking change**: Passing a number with unit `%` to the `$alpha` parameter
+ of `color.change()`, `color.adjust()`, `change-color()`, and `adjust-color()`
+ is now interpreted as a percentage, instead of ignoring the unit. For example,
+ `color.change(red, $alpha: 50%)` now returns `rgb(255 0 0 / 0.5)`.
* Add support for CSS Color Level 4 [color spaces]. Each color value now tracks
its color space along with the values of each channel in that color space.
There are two general principles to keep in mind when dealing with new color
spaces:
-
+
1. With the exception of legacy color spaces (`rgb`, `hsl`, and `hwb`), colors
will always be emitted in the color space they were defined in unless
they're explicitly converted.
@@ -147,7 +152,306 @@
* Added `InterpolationMethod` and `HueInterpolationMethod` which collectively
represent the method to use to interpolate two colors.
-## 1.57.2
+## 1.66.1
+
+### JS API
+
+* Fix a bug where Sass compilation could crash in strict mode if passed a
+ callback that threw a string, boolean, number, symbol, or bignum.
+
+## 1.66.0
+
+* **Breaking change:** Drop support for the additional CSS calculations defined
+ in CSS Values and Units 4. Custom Sass functions whose names overlapped with
+ these new CSS functions were being parsed as CSS calculations instead, causing
+ an unintentional breaking change outside our normal [compatibility policy] for
+ CSS compatibility changes.
+
+ Support will be added again in a future version, but only after Sass has
+ emitted a deprecation warning for all functions that will break for at least
+ three months prior to the breakage.
+
+## 1.65.1
+
+* Update abs-percent deprecatedIn version to `1.65.0`.
+
+## 1.65.0
+
+* All functions defined in CSS Values and Units 4 are now parsed as calculation
+ objects: `round()`, `mod()`, `rem()`, `sin()`, `cos()`, `tan()`, `asin()`,
+ `acos()`, `atan()`, `atan2()`, `pow()`, `sqrt()`, `hypot()`, `log()`, `exp()`,
+ `abs()`, and `sign()`.
+
+* Deprecate explicitly passing the `%` unit to the global `abs()` function. In
+ future releases, this will emit a CSS abs() function to be resolved by the
+ browser. This deprecation is named `abs-percent`.
+
+## 1.64.3
+
+### Dart API
+
+* Deprecate explicitly passing `null` as the alpha channel for
+ `SassColor.rgb()`, `SassColor.hsl()`, and `SassColor.hwb()`. Omitting the
+ `alpha` channel is still allowed. In future releases, `null` will be used to
+ indicate a [missing component]. This deprecation is named `null-alpha`.
+
+ [missing component]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components
+
+* Include protocol buffer definitions when uploading the `sass` package to pub.
+
+### JS API
+
+* Deprecate explicitly passing `null` as the alpha channel for `new
+ SassColor()`. Omitting the `alpha` channel or passing `undefined` for it is
+ still allowed. In future releases, `null` will be used to indicate a [missing
+ component]. This deprecation is named `null-alpha`.
+
+ [missing component]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components
+
+ (Note that this was already prohibited by the TypeScript types, but in
+ practice prior to this `null` was treated as `1`.)
+
+## 1.64.2
+
+* No user-visible changes.
+
+## 1.64.1
+
+### Embedded Sass
+
+* Fix a bug where a valid `SassCalculation.clamp()` with less than 3 arguments
+ would throw an error.
+
+## 1.64.0
+
+* Comments that appear before or between `@use` and `@forward` rules are now
+ emitted in source order as much as possible, instead of always being emitted
+ after the CSS of all module dependencies.
+
+* Fix a bug where an interpolation in a custom property name crashed if the file
+ was loaded by a `@use` nested in an `@import`.
+
+### JavaScript API
+
+* Add a new `SassCalculation` type that represents the calculation objects added
+ in Dart Sass 1.40.0.
+
+* Add `Value.assertCalculation()`, which returns the value if it's a
+ `SassCalculation` and throws an error otherwise.
+
+* Produce a better error message when an environment that supports some Node.js
+ APIs loads the browser entrypoint but attempts to access the filesystem.
+
+### Embedded Sass
+
+* Fix a bug where nested relative `@imports` failed to load when using the
+ deprecated functions `render` or `renderSync` and those relative imports were
+ loaded multiple times across different files.
+
+## 1.63.6
+
+### JavaScript API
+
+* Fix `import sass from 'sass'` again after it was broken in the last release.
+
+### Embedded Sass
+
+* Fix the `exports` declaration in `package.json`.
+
+## 1.63.5
+
+### JavaScript API
+
+* Fix a bug where loading the package through both CJS `require()` and ESM
+ `import` could crash on Node.js.
+
+### Embedded Sass
+
+* Fix a deadlock when running at high concurrency on 32-bit systems.
+
+* Fix a race condition where the embedded compiler could deadlock or crash if a
+ compilation ID was reused immediately after the compilation completed.
+
+## 1.63.4
+
+### JavaScript API
+
+* Re-enable support for `import sass from 'sass'` when loading the package from
+ an ESM module in Node.js. However, this syntax is now deprecated; ESM users
+ should use `import * as sass from 'sass'` instead.
+
+ On the browser and other ESM-only platforms, only `import * as sass from
+ 'sass'` is supported.
+
+* Properly export the legacy API values `TRUE`, `FALSE`, `NULL`, and `types` from
+ the ECMAScript module API.
+
+### Embedded Sass
+
+* Fix a race condition where closing standard input while requests are in-flight
+ could sometimes cause the process to hang rather than shutting down
+ gracefully.
+
+* Properly include the root stylesheet's URL in the set of loaded URLs when it
+ fails to parse.
+
+## 1.63.3
+
+### JavaScript API
+
+* Fix loading Sass as an ECMAScript module on Node.js.
+
+## 1.63.2
+
+* No user-visible changes.
+
+## 1.63.1
+
+* No user-visible changes.
+
+## 1.63.0
+
+### JavaScript API
+
+* Dart Sass's JS API now supports running in the browser. Further details and
+ instructions for use are in [the README](README.md#dart-sass-in-the-browser).
+
+### Embedded Sass
+
+* The Dart Sass embedded compiler is now included as part of the primary Dart
+ Sass distribution, rather than a separate executable. To use the embedded
+ compiler, just run `sass --embedded` from any Sass executable (other than the
+ pure JS executable).
+
+ The Node.js embedded host will still be distributed as the `sass-embedded`
+ package on npm. The only change is that it will now provide direct access to a
+ `sass` executable with the same CLI as the `sass` package.
+
+* The Dart Sass embedded compiler now uses version 2.0.0 of the Sass embedded
+ protocol. See [the spec][embedded-protocol-spec] for a full description of the
+ protocol, and [the changelog][embedded-protocol-changelog] for a summary of
+ changes since version 1.2.0.
+
+ [embedded-protocol-spec]: https://github.com/sass/sass/blob/main/spec/embedded-protocol.md
+ [embedded-protocol-changelog]: https://github.com/sass/sass/blob/main/EMBEDDED_PROTOCOL_CHANGELOG.md
+
+* The Dart Sass embedded compiler now runs multiple simultaneous compilations in
+ parallel, rather than serially.
+
+## 1.62.1
+
+* Fix a bug where `:has(+ &)` and related constructs would drop the leading
+ combinator.
+
+## 1.62.0
+
+* Deprecate the use of multiple `!global` or `!default` flags on the same
+ variable. This deprecation is named `duplicate-var-flags`.
+
+* Allow special numbers like `var()` or `calc()` in the global functions:
+ `grayscale()`, `invert()`, `saturate()`, and `opacity()`. These are also
+ native CSS `filter` functions. This is in addition to number values which were
+ already allowed.
+
+* Fix a cosmetic bug where an outer rule could be duplicated after nesting was
+ resolved, instead of re-using a shared rule.
+
+## 1.61.0
+
+* **Potentially breaking change:** Drop support for End-of-Life Node.js 12.
+
+* Fix remaining cases for the performance regression introduced in 1.59.0.
+
+### Embedded Sass
+
+* The JS embedded host now loads files from the working directory when using the
+ legacy API.
+
+## 1.60.0
+
+* Add support for the `pi`, `e`, `infinity`, `-infinity`, and `NaN` constants in
+ calculations. These will be interpreted as the corresponding numbers.
+
+* Add support for unknown constants in calculations. These will be interpreted
+ as unquoted strings.
+
+* Serialize numbers with value `infinity`, `-infinity`, and `NaN` to `calc()`
+ expressions rather than CSS-invalid identifiers. Numbers with complex units
+ still can't be serialized.
+
+## 1.59.3
+
+* Fix a performance regression introduced in 1.59.0.
+
+* The NPM release of 1.59.0 dropped support for Node 12 without actually
+ indicating so in its pubspec. This release temporarily adds back support so
+ that the latest Sass version that declares it supports Node 12 actually does
+ so. However, Node 12 is now end-of-life, so we will drop support for it
+ properly in an upcoming release.
+
+## 1.59.2
+
+* No user-visible changes.
+
+## 1.59.1
+
+* No user-visible changes.
+
+## 1.59.0
+
+### Command Line Interface
+
+* Added a new `--fatal-deprecation` flag that lets you treat a deprecation
+ warning as an error. You can pass an individual deprecation ID
+ (e.g. `slash-div`) or you can pass a Dart Sass version to treat all
+ deprecations initially emitted in that version or earlier as errors.
+
+* New `--future-deprecation` flag that lets you opt into warning for use of
+ certain features that will be deprecated in the future. At the moment, the
+ only option is `--future-deprecation=import`, which will emit warnings for
+ Sass `@import` rules, which are not yet deprecated, but will be in the future.
+
+### Dart API
+
+* New `Deprecation` enum, which contains the different current and future
+ deprecations used by the new CLI flags.
+
+* The `compile` methods now take in `fatalDeprecations` and `futureDeprecations`
+ parameters, which work similarly to the CLI flags.
+
+## 1.58.4
+
+* Pull `@font-face` to the root rather than bubbling the style rule selector
+ inwards.
+
+* Improve error messages for invalid CSS values passed to plain CSS functions.
+
+* Improve error messages involving selectors.
+
+### Embedded Sass
+
+* Improve the performance of starting up a compilation.
+
+## 1.58.3
+
+* No user-visible changes.
+
+## 1.58.2
+
+### Command Line Interface
+
+* Add a timestamp to messages printed in `--watch` mode.
+
+* Print better `calc()`-based suggestions for `/`-as-division expression that
+ contain calculation-incompatible constructs like unary minus.
+
+## 1.58.1
+
+* Emit a unitless hue when serializing `hsl()` colors. The `deg` unit is
+ incompatible with IE, and while that officially falls outside our
+ compatibility policy, it's better to lean towards greater compatibility.
+
+## 1.58.0
* Remove sourcemap comments from Sass sources. The generated sourcemap comment
for the compiled CSS output remains unaffected.
@@ -160,6 +464,19 @@
* Produce a better error message for a number with a leading `+` or `-`, a
decimal point, but no digits.
+* Produce a better error message for a nested property whose name starts with
+ `--`.
+
+* Fix a crash when a selector ends in an escaped backslash.
+
+* Add the relative length units from CSS Values 4 and CSS Contain 3 as known
+ units to validate bad computation in `calc`.
+
+### Command Line Interface
+
+* The `--watch` flag will now track loads through calls to `meta.load-css()` as
+ long as their URLs are literal strings without any interpolation.
+
## 1.57.1
* No user-visible changes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 24e71bc86..934a0576a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -46,7 +46,7 @@ one above, the
dependencies.
3. [Install Node.js][]. This is only necessary if you're making changes to the
- language or to Dart Sass's Node API.
+ language or to Dart Sass's Node API.
[Install the Dart SDK]: https://www.dartlang.org/install
[Install Node.js]: https://nodejs.org/en/download/
@@ -86,7 +86,7 @@ repository contains language tests that are shared among the main Sass
implementations. Any new feature should be thoroughly tested there, and any bug
should have a regression test added.
-[sass-spec]: http://github.com/sass/sass-spec
+[sass-spec]: https://github.com/sass/sass-spec
To create a new spec:
diff --git a/README.md b/README.md
index c185dc71b..e22309bd4 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun again**.
+A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun**.
@@ -28,10 +28,12 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun again**.
* [Using Dart Sass](#using-dart-sass)
* [From Chocolatey or Scoop (Windows)](#from-chocolatey-or-scoop-windows)
- * [From Homebrew (macOS)](#from-homebrew-macos)
+ * [From Homebrew (macOS)](#from-homebrew-macos-or-linux)
* [Standalone](#standalone)
* [From npm](#from-npm)
+ * [Dart Sass in the Browser](#dart-sass-in-the-browser)
* [Legacy JavaScript API](#legacy-javascript-api)
+ * [Using Sass with Jest](#using-sass-with-jest)
* [From Pub](#from-pub)
* [`sass_api` Package](#sass_api-package)
* [From Source](#from-source)
@@ -40,6 +42,8 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun again**.
* [Compatibility Policy](#compatibility-policy)
* [Browser Compatibility](#browser-compatibility)
* [Node.js Compatibility](#nodejs-compatibility)
+* [Embedded Dart Sass](#embedded-dart-sass)
+ * [Usage](#usage)
* [Behavioral Differences from Ruby Sass](#behavioral-differences-from-ruby-sass)
## Using Dart Sass
@@ -68,9 +72,9 @@ Sass. See [the CLI docs][cli] for details.
[cli]: https://sass-lang.com/documentation/cli/dart-sass
-### From Homebrew (macOS)
+### From Homebrew (macOS or Linux)
-If you use [the Homebrew package manager](https://brew.sh/) for macOS, you
+If you use [the Homebrew package manager](https://brew.sh/), you
can install Dart Sass by running
```sh
@@ -115,6 +119,92 @@ See [the Sass website][js api] for full API documentation.
[js api]: https://sass-lang.com/documentation/js-api
+#### Dart Sass in the Browser
+
+The `sass` npm package can also be run directly in the browser. It's compatible
+with all major web bundlers as long as you disable renaming (such as
+[`--keep-names`] in esbuild). You can also import it directly from a browser as
+an ECMAScript Module without any bundling (assuming `node_modules` is served as
+well):
+
+[`--keep-names`]: https://esbuild.github.io/api/#keep-names
+
+```html
+
+
+
+
+
+
+```
+
+Or from a CDN:
+
+```html
+
+
+
+
+
+
+```
+
+Or even bundled with all its dependencies:
+
+```html
+
+```
+
+Since the browser doesn't have access to the filesystem, the [`compile()`] and
+`compileAsync()` functions aren't available for it. If you want to load other
+files, you'll need to pass a [custom importer] to [`compileString()`] or
+[`compileStringAsync()`]. The [legacy API] is also not supported in the browser.
+
+[`compile()`]: https://sass-lang.com/documentation/js-api/functions/compile
+[`compileAsync()`]: https://sass-lang.com/documentation/js-api/functions/compileAsync
+[custom importer]: https://sass-lang.com/documentation/js-api/interfaces/StringOptionsWithImporter#importer
+[`compileString()`]: https://sass-lang.com/documentation/js-api/functions/compileString
+[`compileStringAsync()`]: https://sass-lang.com/documentation/js-api/functions/compileStringAsync
+[legacy API]: #legacy-javascript-api
+
#### Legacy JavaScript API
Dart Sass also supports an older JavaScript API that's fully compatible with
@@ -123,8 +213,8 @@ Dart Sass also supports an older JavaScript API that's fully compatible with
and will be removed in Dart Sass 2.0.0, so it should be avoided in new projects.
[Node Sass]: https://github.com/sass/node-sass
-[`render()`]: https://sass-lang.com/documentation/js-api/modules#render
-[`renderSync()`]: https://sass-lang.com/documentation/js-api/modules#renderSync
+[`render()`]: https://sass-lang.com/documentation/js-api/functions/render
+[`renderSync()`]: https://sass-lang.com/documentation/js-api/functions/renderSync
Sass's support for the legacy JavaScript API has the following limitations:
@@ -192,10 +282,19 @@ Assuming you've already checked out this repository:
manually rather than using an installer, make sure the SDK's `bin` directory
is on your `PATH`.
-2. In this repository, run `pub get`. This will install Dart Sass's
+2. [Install Buf]. This is used to build the protocol buffers for the [embedded
+ compiler].
+
+3. In this repository, run `dart pub get`. This will install Dart Sass's
dependencies.
-3. Run `dart bin/sass.dart path/to/file.scss`.
+4. Run `dart run grinder protobuf`. This will download and build the embedded
+ protocol definition.
+
+5. Run `dart bin/sass.dart path/to/file.scss`.
+
+[Install Buf]: https://docs.buf.build/installation
+[embedded compiler]: #embedded-dart-sass
That's it!
@@ -207,13 +306,20 @@ commands:
```Dockerfile
# Dart stage
FROM dart:stable AS dart
+FROM bufbuild/buf AS buf
+# Add your scss files
COPY --from=another_stage /app /app
+# Include Protocol Buffer binary
+COPY --from=buf /usr/local/bin/buf /usr/local/bin/
+
WORKDIR /dart-sass
RUN git clone https://github.com/sass/dart-sass.git . && \
dart pub get && \
- dart ./bin/sass.dart /app/sass/example.scss /app/public/css/example.css
+ dart run grinder protobuf
+# This is where you run sass.dart on your scss file(s)
+RUN dart ./bin/sass.dart /app/sass/example.scss /app/public/css/example.css
```
## Why Dart?
@@ -299,6 +405,26 @@ considers itself free to break support if necessary.
[the Node.js release page]: https://nodejs.org/en/about/releases/
+## Embedded Dart Sass
+
+Dart Sass includes an implementation of the compiler side of the [Embedded Sass
+protocol]. It's designed to be embedded in a host language, which then exposes
+an API for users to invoke Sass and define custom functions and importers.
+
+[Embedded Sass protocol]: https://github.com/sass/sass/blob/main/spec/embedded-protocol.md
+
+### Usage
+
+* `sass --embedded` starts the embedded compiler and listens on stdin.
+* `sass --embedded --version` prints `versionResponse` with `id = 0` in JSON and
+ exits.
+
+The `--embedded` command-line flag is not available when you install Dart Sass
+as an [npm package]. No other command-line flags are supported with
+`--embedded`.
+
+[npm package]: #from-npm
+
## Behavioral Differences from Ruby Sass
There are a few intentional behavioral differences between Dart Sass and Ruby
diff --git a/analysis/lib/analysis_options.yaml b/analysis/lib/analysis_options.yaml
index 4ab629352..2a2921abc 100644
--- a/analysis/lib/analysis_options.yaml
+++ b/analysis/lib/analysis_options.yaml
@@ -1,8 +1,7 @@
analyzer:
exclude: [build/**]
- strong-mode:
- implicit-casts: false
language:
+ strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
diff --git a/analysis_options.yaml b/analysis_options.yaml
index fdd023b43..36aac758f 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -4,3 +4,5 @@
# out-of-date (because they cause "pub run" to modify the lockfile before it
# runs the executable).
include: analysis/lib/analysis_options.yaml
+analyzer:
+ exclude: ['**/*.pb*.dart']
diff --git a/bin/sass.dart b/bin/sass.dart
index a95ad65ba..986ff9bcb 100644
--- a/bin/sass.dart
+++ b/bin/sass.dart
@@ -15,8 +15,15 @@ import 'package:sass/src/executable/repl.dart';
import 'package:sass/src/executable/watch.dart';
import 'package:sass/src/import_cache.dart';
import 'package:sass/src/io.dart';
+import 'package:sass/src/io.dart' as io;
+import 'package:sass/src/logger/deprecation_handling.dart';
import 'package:sass/src/stylesheet_graph.dart';
+import 'package:sass/src/util/map.dart';
import 'package:sass/src/utils.dart';
+import 'package:sass/src/embedded/executable.dart'
+ // Never load the embedded protocol when compiling to JS.
+ if (dart.library.js) 'package:sass/src/embedded/unavailable.dart'
+ as embedded;
Future main(List args) async {
var printedError = false;
@@ -26,14 +33,23 @@ Future main(List args) async {
//
// If [trace] is passed, its terse representation is printed after the error.
void printError(String error, StackTrace? stackTrace) {
- if (printedError) stderr.writeln();
+ var buffer = StringBuffer();
+ if (printedError) buffer.writeln();
printedError = true;
- stderr.writeln(error);
+ buffer.write(error);
if (stackTrace != null) {
- stderr.writeln();
- stderr.writeln(Trace.from(stackTrace).terse.toString().trimRight());
+ buffer.writeln();
+ buffer.writeln();
+ buffer.write(Trace.from(stackTrace).terse.toString().trimRight());
}
+
+ io.printError(buffer);
+ }
+
+ if (args case ['--embedded', ...var rest]) {
+ embedded.main(rest);
+ return;
}
ExecutableOptions? options;
@@ -52,38 +68,36 @@ Future main(List args) async {
return;
}
- var graph = StylesheetGraph(
- ImportCache(loadPaths: options.loadPaths, logger: options.logger));
+ var graph = StylesheetGraph(ImportCache(
+ loadPaths: options.loadPaths,
+ // This logger is only used for handling fatal/future deprecations
+ // during parsing, and is re-used across parses, so we don't want to
+ // limit repetition. A separate DeprecationHandlingLogger is created for
+ // each compilation, which will limit repetition if verbose is not
+ // passed in addition to handling fatal/future deprecations.
+ logger: DeprecationHandlingLogger(options.logger,
+ fatalDeprecations: options.fatalDeprecations,
+ futureDeprecations: options.futureDeprecations,
+ limitRepetition: false)));
if (options.watch) {
await watch(options, graph);
return;
}
- for (var source in options.sourcesToDestinations.keys) {
- var destination = options.sourcesToDestinations[source];
+ for (var (source, destination) in options.sourcesToDestinations.pairs) {
try {
await compileStylesheet(options, graph, source, destination,
ifModified: options.update);
} on SassException catch (error, stackTrace) {
- // This is an immediately-invoked function expression to work around
- // dart-lang/sdk#33400.
- () {
- try {
- if (destination != null &&
- // dart-lang/sdk#45348
- !options!.emitErrorCss) {
- deleteFile(destination);
- }
- } on FileSystemException {
- // If the file doesn't exist, that's fine.
- }
- }();
+ if (destination != null && !options.emitErrorCss) {
+ _tryDelete(destination);
+ }
printError(error.toString(color: options.color),
options.trace ? getTrace(error) ?? stackTrace : null);
// Exit code 65 indicates invalid data per
- // http://www.freebsd.org/cgi/man.cgi?query=sysexits.
+ // https://www.freebsd.org/cgi/man.cgi?query=sysexits.
//
// We let exitCode 66 take precedence for deterministic behavior.
if (exitCode != 66) exitCode = 65;
@@ -109,9 +123,9 @@ Future main(List args) async {
exitCode = 64;
} catch (error, stackTrace) {
var buffer = StringBuffer();
- if (options != null && options.color) buffer.write('\u001b[31m\u001b[1m');
+ if (options?.color ?? false) buffer.write('\u001b[31m\u001b[1m');
buffer.write('Unexpected exception:');
- if (options != null && options.color) buffer.write('\u001b[0m');
+ if (options?.color ?? false) buffer.write('\u001b[0m');
buffer.writeln();
buffer.writeln(error);
@@ -140,3 +154,14 @@ Future _loadVersion() async {
.split(" ")
.last;
}
+
+/// Delete [path] if it exists and do nothing otherwise.
+///
+/// This is a separate function to work around dart-lang/sdk#53082.
+void _tryDelete(String path) {
+ try {
+ deleteFile(path);
+ } on FileSystemException {
+ // If the file doesn't exist, that's fine.
+ }
+}
diff --git a/buf.gen.yaml b/buf.gen.yaml
new file mode 100644
index 000000000..2fd379361
--- /dev/null
+++ b/buf.gen.yaml
@@ -0,0 +1,4 @@
+version: v1
+plugins:
+- plugin: dart
+ out: lib/src/embedded
diff --git a/buf.work.yaml b/buf.work.yaml
new file mode 100644
index 000000000..3ffcae58c
--- /dev/null
+++ b/buf.work.yaml
@@ -0,0 +1,2 @@
+version: v1
+directories: [build/language/spec]
diff --git a/lib/sass.dart b/lib/sass.dart
index 157383dee..bc7487da1 100644
--- a/lib/sass.dart
+++ b/lib/sass.dart
@@ -13,6 +13,7 @@ import 'src/async_import_cache.dart';
import 'src/callable.dart';
import 'src/compile.dart' as c;
import 'src/compile_result.dart';
+import 'src/deprecation.dart';
import 'src/exception.dart';
import 'src/import_cache.dart';
import 'src/importer.dart';
@@ -24,9 +25,10 @@ import 'src/visitor/serialize.dart';
export 'src/callable.dart' show Callable, AsyncCallable;
export 'src/compile_result.dart';
+export 'src/deprecation.dart';
export 'src/exception.dart' show SassException;
export 'src/importer.dart';
-export 'src/logger.dart';
+export 'src/logger.dart' show Logger;
export 'src/syntax.dart';
export 'src/value.dart'
hide
@@ -110,7 +112,9 @@ CompileResult compileToResult(String path,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) =>
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) =>
c.compile(path,
logger: logger,
importCache: ImportCache(
@@ -123,7 +127,9 @@ CompileResult compileToResult(String path,
quietDeps: quietDeps,
verbose: verbose,
sourceMap: sourceMap,
- charset: charset);
+ charset: charset,
+ fatalDeprecations: fatalDeprecations,
+ futureDeprecations: futureDeprecations);
/// Compiles [source] to CSS and returns a [CompileResult] containing the CSS
/// and additional metadata about the compilation..
@@ -205,7 +211,9 @@ CompileResult compileStringToResult(String source,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) =>
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) =>
c.compileString(source,
syntax: syntax,
logger: logger,
@@ -221,7 +229,9 @@ CompileResult compileStringToResult(String source,
quietDeps: quietDeps,
verbose: verbose,
sourceMap: sourceMap,
- charset: charset);
+ charset: charset,
+ fatalDeprecations: fatalDeprecations,
+ futureDeprecations: futureDeprecations);
/// Like [compileToResult], except it runs asynchronously.
///
@@ -239,7 +249,9 @@ Future compileToResultAsync(String path,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) =>
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) =>
c.compileAsync(path,
logger: logger,
importCache: AsyncImportCache(
@@ -252,7 +264,9 @@ Future compileToResultAsync(String path,
quietDeps: quietDeps,
verbose: verbose,
sourceMap: sourceMap,
- charset: charset);
+ charset: charset,
+ fatalDeprecations: fatalDeprecations,
+ futureDeprecations: futureDeprecations);
/// Like [compileStringToResult], except it runs asynchronously.
///
@@ -275,7 +289,9 @@ Future compileStringToResultAsync(String source,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) =>
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) =>
c.compileStringAsync(source,
syntax: syntax,
logger: logger,
@@ -315,8 +331,7 @@ Future compileStringToResultAsync(String source,
///
/// {@category Compile}
@Deprecated("Use compileToResult() instead.")
-String compile(
- String path,
+String compile(String path,
{bool color = false,
Logger? logger,
Iterable? importers,
@@ -327,7 +342,7 @@ String compile(
bool quietDeps = false,
bool verbose = false,
@Deprecated("Use CompileResult.sourceMap from compileToResult() instead.")
- void sourceMap(SingleMapping map)?,
+ void sourceMap(SingleMapping map)?,
bool charset = true}) {
var result = compileToResult(path,
logger: logger,
@@ -366,8 +381,7 @@ String compile(
///
/// {@category Compile}
@Deprecated("Use compileStringToResult() instead.")
-String compileString(
- String source,
+String compileString(String source,
{Syntax? syntax,
bool color = false,
Logger? logger,
@@ -380,11 +394,11 @@ String compileString(
Object? url,
bool quietDeps = false,
bool verbose = false,
- @Deprecated("Use CompileResult.sourceMap from compileStringToResult() instead.")
- void sourceMap(SingleMapping map)?,
+ @Deprecated(
+ "Use CompileResult.sourceMap from compileStringToResult() instead.")
+ void sourceMap(SingleMapping map)?,
bool charset = true,
- @Deprecated("Use syntax instead.")
- bool indented = false}) {
+ @Deprecated("Use syntax instead.") bool indented = false}) {
var result = compileStringToResult(source,
syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss),
logger: logger,
@@ -411,8 +425,7 @@ String compileString(
///
/// {@category Compile}
@Deprecated("Use compileToResultAsync() instead.")
-Future compileAsync(
- String path,
+Future compileAsync(String path,
{bool color = false,
Logger? logger,
Iterable? importers,
@@ -422,8 +435,9 @@ Future compileAsync(
OutputStyle? style,
bool quietDeps = false,
bool verbose = false,
- @Deprecated("Use CompileResult.sourceMap from compileToResultAsync() instead.")
- void sourceMap(SingleMapping map)?}) async {
+ @Deprecated(
+ "Use CompileResult.sourceMap from compileToResultAsync() instead.")
+ void sourceMap(SingleMapping map)?}) async {
var result = await compileToResultAsync(path,
logger: logger,
importers: importers,
@@ -446,8 +460,7 @@ Future compileAsync(
///
/// {@category Compile}
@Deprecated("Use compileStringToResultAsync() instead.")
-Future compileStringAsync(
- String source,
+Future compileStringAsync(String source,
{Syntax? syntax,
bool color = false,
Logger? logger,
@@ -460,11 +473,11 @@ Future compileStringAsync(
Object? url,
bool quietDeps = false,
bool verbose = false,
- @Deprecated("Use CompileResult.sourceMap from compileStringToResultAsync() instead.")
- void sourceMap(SingleMapping map)?,
+ @Deprecated(
+ "Use CompileResult.sourceMap from compileStringToResultAsync() instead.")
+ void sourceMap(SingleMapping map)?,
bool charset = true,
- @Deprecated("Use syntax instead.")
- bool indented = false}) async {
+ @Deprecated("Use syntax instead.") bool indented = false}) async {
var result = await compileStringToResultAsync(source,
syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss),
logger: logger,
diff --git a/lib/src/ast/css/at_rule.dart b/lib/src/ast/css/at_rule.dart
index 8aff6529e..f11aa253f 100644
--- a/lib/src/ast/css/at_rule.dart
+++ b/lib/src/ast/css/at_rule.dart
@@ -2,12 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'node.dart';
import 'value.dart';
/// An unknown plain CSS at-rule.
-abstract class CssAtRule extends CssParentNode {
+abstract interface class CssAtRule implements CssParentNode {
/// The name of this rule.
CssValue get name;
@@ -19,6 +18,4 @@ abstract class CssAtRule extends CssParentNode {
/// This implies `children.isEmpty`, but the reverse is not true—for a rule
/// like `@foo {}`, [children] is empty but [isChildless] is `false`.
bool get isChildless;
-
- T accept(CssVisitor visitor) => visitor.visitCssAtRule(this);
}
diff --git a/lib/src/ast/css/comment.dart b/lib/src/ast/css/comment.dart
index 4724a83b2..39d76423c 100644
--- a/lib/src/ast/css/comment.dart
+++ b/lib/src/ast/css/comment.dart
@@ -2,19 +2,16 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'node.dart';
/// A plain CSS comment.
///
/// This is always a multi-line comment.
-abstract class CssComment extends CssNode {
+abstract interface class CssComment implements CssNode {
/// The contents of this comment, including `/*` and `*/`.
String get text;
/// Whether this comment starts with `/*!` and so should be preserved even in
/// compressed mode.
bool get isPreserved;
-
- T accept(CssVisitor visitor) => visitor.visitCssComment(this);
}
diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart
index 1bd7279fc..4d5e906cd 100644
--- a/lib/src/ast/css/declaration.dart
+++ b/lib/src/ast/css/declaration.dart
@@ -5,12 +5,11 @@
import 'package:source_span/source_span.dart';
import '../../value.dart';
-import '../../visitor/interface/css.dart';
import 'node.dart';
import 'value.dart';
/// A plain CSS declaration (that is, a `name: value` pair).
-abstract class CssDeclaration extends CssNode {
+abstract interface class CssDeclaration implements CssNode {
/// The name of this declaration.
CssValue get name;
@@ -34,6 +33,4 @@ abstract class CssDeclaration extends CssNode {
/// If this is `true`, [isCustomProperty] will also be `true` and [value] will
/// contain a [SassString].
bool get parsedAsCustomProperty;
-
- T accept(CssVisitor visitor);
}
diff --git a/lib/src/ast/css/import.dart b/lib/src/ast/css/import.dart
index b8ad9e9e0..527006b7e 100644
--- a/lib/src/ast/css/import.dart
+++ b/lib/src/ast/css/import.dart
@@ -2,12 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'node.dart';
import 'value.dart';
/// A plain CSS `@import`.
-abstract class CssImport extends CssNode {
+abstract interface class CssImport implements CssNode {
/// The URL being imported.
///
/// This includes quotes.
@@ -15,6 +14,4 @@ abstract class CssImport extends CssNode {
/// The modifiers (such as media or supports queries) attached to this import.
CssValue? get modifiers;
-
- T accept(CssVisitor visitor) => visitor.visitCssImport(this);
}
diff --git a/lib/src/ast/css/keyframe_block.dart b/lib/src/ast/css/keyframe_block.dart
index 0b52cd82d..c6255fe45 100644
--- a/lib/src/ast/css/keyframe_block.dart
+++ b/lib/src/ast/css/keyframe_block.dart
@@ -2,16 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'node.dart';
import 'value.dart';
/// A block within a `@keyframes` rule.
///
/// For example, `10% {opacity: 0.5}`.
-abstract class CssKeyframeBlock extends CssParentNode {
+abstract interface class CssKeyframeBlock implements CssParentNode {
/// The selector for this block.
CssValue> get selector;
-
- T accept(CssVisitor visitor) => visitor.visitCssKeyframeBlock(this);
}
diff --git a/lib/src/ast/css/media_query.dart b/lib/src/ast/css/media_query.dart
index 8a095622d..dc2d5d532 100644
--- a/lib/src/ast/css/media_query.dart
+++ b/lib/src/ast/css/media_query.dart
@@ -2,12 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
+import '../../interpolation_map.dart';
import '../../logger.dart';
import '../../parse/media_query.dart';
import '../../utils.dart';
/// A plain CSS media query, as used in `@media` and `@import`.
-class CssMediaQuery {
+final class CssMediaQuery {
/// The modifier, probably either "not" or "only".
///
/// This may be `null` if no modifier is in use.
@@ -43,8 +44,10 @@ class CssMediaQuery {
///
/// Throws a [SassFormatException] if parsing fails.
static List parseList(String contents,
- {Object? url, Logger? logger}) =>
- MediaQueryParser(contents, url: url, logger: logger).parse();
+ {Object? url, Logger? logger, InterpolationMap? interpolationMap}) =>
+ MediaQueryParser(contents,
+ url: url, logger: logger, interpolationMap: interpolationMap)
+ .parse();
/// Creates a media query specifies a type and, optionally, conditions.
///
@@ -194,25 +197,21 @@ class CssMediaQuery {
///
/// This is either the singleton values [empty] or [unrepresentable], or an
/// instance of [MediaQuerySuccessfulMergeResult].
-abstract class MediaQueryMergeResult {
+sealed class MediaQueryMergeResult {
/// A singleton value indicating that there are no contexts that match both
/// input queries.
- static const empty = _SingletonCssMediaQueryMergeResult("empty");
+ static const empty = _SingletonCssMediaQueryMergeResult.empty;
/// A singleton value indicating that the contexts that match both input
/// queries can't be represented by a Level 3 media query.
static const unrepresentable =
- _SingletonCssMediaQueryMergeResult("unrepresentable");
+ _SingletonCssMediaQueryMergeResult.unrepresentable;
}
/// The subclass [MediaQueryMergeResult] that represents singleton enum values.
-class _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult {
- /// The name of the result type.
- final String _name;
-
- const _SingletonCssMediaQueryMergeResult(this._name);
-
- String toString() => _name;
+enum _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult {
+ empty,
+ unrepresentable;
}
/// A successful result of [CssMediaQuery.merge].
diff --git a/lib/src/ast/css/media_rule.dart b/lib/src/ast/css/media_rule.dart
index d44859fb1..8eeba7e01 100644
--- a/lib/src/ast/css/media_rule.dart
+++ b/lib/src/ast/css/media_rule.dart
@@ -2,16 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'media_query.dart';
import 'node.dart';
/// A plain CSS `@media` rule.
-abstract class CssMediaRule extends CssParentNode {
+abstract interface class CssMediaRule implements CssParentNode {
/// The queries for this rule.
///
/// This is never empty.
List get queries;
-
- T accept(CssVisitor visitor) => visitor.visitCssMediaRule(this);
}
diff --git a/lib/src/ast/css/modifiable.dart b/lib/src/ast/css/modifiable.dart
index 3d9ec990d..c2518811b 100644
--- a/lib/src/ast/css/modifiable.dart
+++ b/lib/src/ast/css/modifiable.dart
@@ -12,4 +12,3 @@ export 'modifiable/node.dart';
export 'modifiable/style_rule.dart';
export 'modifiable/stylesheet.dart';
export 'modifiable/supports_rule.dart';
-export 'modifiable/value.dart';
diff --git a/lib/src/ast/css/modifiable/at_rule.dart b/lib/src/ast/css/modifiable/at_rule.dart
index e616de5c2..a63579688 100644
--- a/lib/src/ast/css/modifiable/at_rule.dart
+++ b/lib/src/ast/css/modifiable/at_rule.dart
@@ -10,7 +10,8 @@ import '../value.dart';
import 'node.dart';
/// A modifiable version of [CssAtRule] for use in the evaluation step.
-class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule {
+final class ModifiableCssAtRule extends ModifiableCssParentNode
+ implements CssAtRule {
final CssValue name;
final CssValue? value;
final bool isChildless;
@@ -22,6 +23,12 @@ class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule {
T accept(ModifiableCssVisitor visitor) => visitor.visitCssAtRule(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssAtRule &&
+ name == other.name &&
+ value == other.value &&
+ isChildless == other.isChildless;
+
ModifiableCssAtRule copyWithoutChildren() =>
ModifiableCssAtRule(name, span, childless: isChildless, value: value);
diff --git a/lib/src/ast/css/modifiable/comment.dart b/lib/src/ast/css/modifiable/comment.dart
index 40e7838c7..a795b1d00 100644
--- a/lib/src/ast/css/modifiable/comment.dart
+++ b/lib/src/ast/css/modifiable/comment.dart
@@ -10,7 +10,8 @@ import '../comment.dart';
import 'node.dart';
/// A modifiable version of [CssComment] for use in the evaluation step.
-class ModifiableCssComment extends ModifiableCssNode implements CssComment {
+final class ModifiableCssComment extends ModifiableCssNode
+ implements CssComment {
final String text;
final FileSpan span;
diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart
index 1082e1a48..1acb292a7 100644
--- a/lib/src/ast/css/modifiable/declaration.dart
+++ b/lib/src/ast/css/modifiable/declaration.dart
@@ -11,7 +11,7 @@ import '../value.dart';
import 'node.dart';
/// A modifiable version of [CssDeclaration] for use in the evaluation step.
-class ModifiableCssDeclaration extends ModifiableCssNode
+final class ModifiableCssDeclaration extends ModifiableCssNode
implements CssDeclaration {
final CssValue name;
final CssValue value;
diff --git a/lib/src/ast/css/modifiable/import.dart b/lib/src/ast/css/modifiable/import.dart
index 033424e78..24de1fb17 100644
--- a/lib/src/ast/css/modifiable/import.dart
+++ b/lib/src/ast/css/modifiable/import.dart
@@ -10,7 +10,7 @@ import '../value.dart';
import 'node.dart';
/// A modifiable version of [CssImport] for use in the evaluation step.
-class ModifiableCssImport extends ModifiableCssNode implements CssImport {
+final class ModifiableCssImport extends ModifiableCssNode implements CssImport {
/// The URL being imported.
///
/// This includes quotes.
diff --git a/lib/src/ast/css/modifiable/keyframe_block.dart b/lib/src/ast/css/modifiable/keyframe_block.dart
index 69233f355..6b7beb9e2 100644
--- a/lib/src/ast/css/modifiable/keyframe_block.dart
+++ b/lib/src/ast/css/modifiable/keyframe_block.dart
@@ -4,13 +4,14 @@
import 'package:source_span/source_span.dart';
+import '../../../utils.dart';
import '../../../visitor/interface/modifiable_css.dart';
import '../keyframe_block.dart';
import '../value.dart';
import 'node.dart';
/// A modifiable version of [CssKeyframeBlock] for use in the evaluation step.
-class ModifiableCssKeyframeBlock extends ModifiableCssParentNode
+final class ModifiableCssKeyframeBlock extends ModifiableCssParentNode
implements CssKeyframeBlock {
final CssValue> selector;
final FileSpan span;
@@ -20,6 +21,10 @@ class ModifiableCssKeyframeBlock extends ModifiableCssParentNode
T accept(ModifiableCssVisitor visitor) =>
visitor.visitCssKeyframeBlock(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssKeyframeBlock &&
+ listEquals(selector.value, other.selector.value);
+
ModifiableCssKeyframeBlock copyWithoutChildren() =>
ModifiableCssKeyframeBlock(selector, span);
}
diff --git a/lib/src/ast/css/modifiable/media_rule.dart b/lib/src/ast/css/modifiable/media_rule.dart
index f1dcf25e4..e30f44b29 100644
--- a/lib/src/ast/css/modifiable/media_rule.dart
+++ b/lib/src/ast/css/modifiable/media_rule.dart
@@ -4,13 +4,14 @@
import 'package:source_span/source_span.dart';
+import '../../../utils.dart';
import '../../../visitor/interface/modifiable_css.dart';
import '../media_query.dart';
import '../media_rule.dart';
import 'node.dart';
/// A modifiable version of [CssMediaRule] for use in the evaluation step.
-class ModifiableCssMediaRule extends ModifiableCssParentNode
+final class ModifiableCssMediaRule extends ModifiableCssParentNode
implements CssMediaRule {
final List queries;
final FileSpan span;
@@ -25,6 +26,9 @@ class ModifiableCssMediaRule extends ModifiableCssParentNode
T accept(ModifiableCssVisitor visitor) =>
visitor.visitCssMediaRule(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssMediaRule && listEquals(queries, other.queries);
+
ModifiableCssMediaRule copyWithoutChildren() =>
ModifiableCssMediaRule(queries, span);
}
diff --git a/lib/src/ast/css/modifiable/node.dart b/lib/src/ast/css/modifiable/node.dart
index 0f4495f32..bef0be821 100644
--- a/lib/src/ast/css/modifiable/node.dart
+++ b/lib/src/ast/css/modifiable/node.dart
@@ -12,7 +12,7 @@ import '../node.dart';
/// Almost all CSS nodes are the modifiable classes under the covers. However,
/// modification should only be done within the evaluation step, so the
/// unmodifiable types are used elsewhere to enforce that constraint.
-abstract class ModifiableCssNode extends CssNode {
+abstract base class ModifiableCssNode extends CssNode {
/// The node that contains this, or `null` for the root [CssStylesheet] node.
ModifiableCssParentNode? get parent => _parent;
ModifiableCssParentNode? _parent;
@@ -43,8 +43,7 @@ abstract class ModifiableCssNode extends CssNode {
}
parent._children.removeAt(_indexInParent!);
- for (var i = _indexInParent!; i < parent._children.length; i++) {
- var child = parent._children[i];
+ for (var child in parent._children.skip(_indexInParent!)) {
child._indexInParent = child._indexInParent! - 1;
}
_parent = null;
@@ -52,7 +51,7 @@ abstract class ModifiableCssNode extends CssNode {
}
/// A modifiable version of [CssParentNode] for use in the evaluation step.
-abstract class ModifiableCssParentNode extends ModifiableCssNode
+abstract base class ModifiableCssParentNode extends ModifiableCssNode
implements CssParentNode {
final List children;
final List _children;
@@ -66,6 +65,9 @@ abstract class ModifiableCssParentNode extends ModifiableCssNode
: _children = children,
children = UnmodifiableListView(children);
+ /// Returns whether [this] is equal to [other], ignoring their child nodes.
+ bool equalsIgnoringChildren(ModifiableCssNode other);
+
/// Returns a copy of [this] with an empty [children] list.
///
/// This is *not* a deep copy. If other parts of this node are modifiable,
@@ -78,4 +80,13 @@ abstract class ModifiableCssParentNode extends ModifiableCssNode
child._indexInParent = _children.length;
_children.add(child);
}
+
+ /// Destructively removes all elements from [children].
+ void clearChildren() {
+ for (var child in _children) {
+ child._parent = null;
+ child._indexInParent = null;
+ }
+ _children.clear();
+ }
}
diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart
index 41400be70..a5d2b1f0c 100644
--- a/lib/src/ast/css/modifiable/style_rule.dart
+++ b/lib/src/ast/css/modifiable/style_rule.dart
@@ -4,30 +4,38 @@
import 'package:source_span/source_span.dart';
+import '../../../util/box.dart';
import '../../../visitor/interface/modifiable_css.dart';
import '../../selector.dart';
import '../style_rule.dart';
import 'node.dart';
-import 'value.dart';
/// A modifiable version of [CssStyleRule] for use in the evaluation step.
-class ModifiableCssStyleRule extends ModifiableCssParentNode
+final class ModifiableCssStyleRule extends ModifiableCssParentNode
implements CssStyleRule {
- final ModifiableCssValue selector;
+ SelectorList get selector => _selector.value;
+
+ /// A reference to the modifiable selector list provided by the extension
+ /// store, which may update it over time as new extensions are applied.
+ final Box _selector;
+
final SelectorList originalSelector;
final FileSpan span;
/// Creates a new [ModifiableCssStyleRule].
///
- /// If [originalSelector] isn't passed, it defaults to [selector.value].
- ModifiableCssStyleRule(this.selector, this.span,
+ /// If [originalSelector] isn't passed, it defaults to [_selector.value].
+ ModifiableCssStyleRule(this._selector, this.span,
{SelectorList? originalSelector})
- : originalSelector = originalSelector ?? selector.value;
+ : originalSelector = originalSelector ?? _selector.value;
T accept(ModifiableCssVisitor visitor) =>
visitor.visitCssStyleRule(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssStyleRule && other.selector == selector;
+
ModifiableCssStyleRule copyWithoutChildren() =>
- ModifiableCssStyleRule(selector, span,
+ ModifiableCssStyleRule(_selector, span,
originalSelector: originalSelector);
}
diff --git a/lib/src/ast/css/modifiable/stylesheet.dart b/lib/src/ast/css/modifiable/stylesheet.dart
index 46610a948..08f598b35 100644
--- a/lib/src/ast/css/modifiable/stylesheet.dart
+++ b/lib/src/ast/css/modifiable/stylesheet.dart
@@ -9,7 +9,7 @@ import '../stylesheet.dart';
import 'node.dart';
/// A modifiable version of [CssStylesheet] for use in the evaluation step.
-class ModifiableCssStylesheet extends ModifiableCssParentNode
+final class ModifiableCssStylesheet extends ModifiableCssParentNode
implements CssStylesheet {
final FileSpan span;
@@ -18,6 +18,9 @@ class ModifiableCssStylesheet extends ModifiableCssParentNode
T accept(ModifiableCssVisitor visitor) =>
visitor.visitCssStylesheet(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssStylesheet;
+
ModifiableCssStylesheet copyWithoutChildren() =>
ModifiableCssStylesheet(span);
}
diff --git a/lib/src/ast/css/modifiable/supports_rule.dart b/lib/src/ast/css/modifiable/supports_rule.dart
index ef921b511..6f3419dff 100644
--- a/lib/src/ast/css/modifiable/supports_rule.dart
+++ b/lib/src/ast/css/modifiable/supports_rule.dart
@@ -10,7 +10,7 @@ import '../value.dart';
import 'node.dart';
/// A modifiable version of [CssSupportsRule] for use in the evaluation step.
-class ModifiableCssSupportsRule extends ModifiableCssParentNode
+final class ModifiableCssSupportsRule extends ModifiableCssParentNode
implements CssSupportsRule {
final CssValue condition;
final FileSpan span;
@@ -20,6 +20,9 @@ class ModifiableCssSupportsRule extends ModifiableCssParentNode
T accept(ModifiableCssVisitor visitor) =>
visitor.visitCssSupportsRule(this);
+ bool equalsIgnoringChildren(ModifiableCssNode other) =>
+ other is ModifiableCssSupportsRule && condition == other.condition;
+
ModifiableCssSupportsRule copyWithoutChildren() =>
ModifiableCssSupportsRule(condition, span);
}
diff --git a/lib/src/ast/css/modifiable/value.dart b/lib/src/ast/css/modifiable/value.dart
deleted file mode 100644
index 2f29676be..000000000
--- a/lib/src/ast/css/modifiable/value.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2019 Google Inc. Use of this source code is governed by an
-// MIT-style license that can be found in the LICENSE file or at
-// https://opensource.org/licenses/MIT.
-
-import 'package:source_span/source_span.dart';
-
-import '../value.dart';
-
-/// A modifiable version of [CssValue] for use in the evaluation step.
-class ModifiableCssValue implements CssValue {
- T value;
- final FileSpan span;
-
- ModifiableCssValue(this.value, this.span);
-
- String toString() => value.toString();
-}
diff --git a/lib/src/ast/css/node.dart b/lib/src/ast/css/node.dart
index 0ca31890c..29daba28d 100644
--- a/lib/src/ast/css/node.dart
+++ b/lib/src/ast/css/node.dart
@@ -13,7 +13,8 @@ import 'comment.dart';
import 'style_rule.dart';
/// A statement in a plain CSS syntax tree.
-abstract class CssNode extends AstNode {
+@sealed
+abstract class CssNode implements AstNode {
/// Whether this was generated from the last node in a nested Sass tree that
/// got flattened during evaluation.
bool get isGroupEnd;
@@ -43,12 +44,13 @@ abstract class CssNode extends AstNode {
bool get isInvisibleHidingComments => accept(
const _IsInvisibleVisitor(includeBogus: true, includeComments: true));
- String toString() => serialize(this, inspect: true).css;
+ String toString() => serialize(this, inspect: true).$1;
}
// NOTE: New at-rule implementations should add themselves to [AtRootRule]'s
// exclude logic.
/// A [CssNode] that can have child statements.
+@sealed
abstract class CssParentNode extends CssNode {
/// The child statements of this node.
List get children;
@@ -82,7 +84,7 @@ class _IsInvisibleVisitor with EveryCssVisitor {
bool visitCssStyleRule(CssStyleRule rule) =>
(includeBogus
- ? rule.selector.value.isInvisible
- : rule.selector.value.isInvisibleOtherThanBogusCombinators) ||
+ ? rule.selector.isInvisible
+ : rule.selector.isInvisibleOtherThanBogusCombinators) ||
super.visitCssStyleRule(rule);
}
diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart
index 2a902efc3..ccce74fdb 100644
--- a/lib/src/ast/css/style_rule.dart
+++ b/lib/src/ast/css/style_rule.dart
@@ -2,22 +2,18 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import '../selector.dart';
import 'node.dart';
-import 'value.dart';
/// A plain CSS style rule.
///
/// This applies style declarations to elements that match a given selector.
/// Note that this isn't *strictly* plain CSS, since [selector] may still
/// contain placeholder selectors.
-abstract class CssStyleRule extends CssParentNode {
+abstract interface class CssStyleRule implements CssParentNode {
/// The selector for this rule.
- CssValue get selector;
+ SelectorList get selector;
/// The selector for this rule, before any extensions were applied.
SelectorList get originalSelector;
-
- T accept(CssVisitor visitor) => visitor.visitCssStyleRule(this);
}
diff --git a/lib/src/ast/css/supports_rule.dart b/lib/src/ast/css/supports_rule.dart
index 091ba1dad..76457bc67 100644
--- a/lib/src/ast/css/supports_rule.dart
+++ b/lib/src/ast/css/supports_rule.dart
@@ -2,14 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import '../../visitor/interface/css.dart';
import 'node.dart';
import 'value.dart';
/// A plain CSS `@supports` rule.
-abstract class CssSupportsRule extends CssParentNode {
+abstract interface class CssSupportsRule implements CssParentNode {
/// The supports condition.
CssValue get condition;
-
- T accept(CssVisitor visitor) => visitor.visitCssSupportsRule(this);
}
diff --git a/lib/src/ast/css/value.dart b/lib/src/ast/css/value.dart
index ce8ee2689..0cda62a78 100644
--- a/lib/src/ast/css/value.dart
+++ b/lib/src/ast/css/value.dart
@@ -9,8 +9,8 @@ import '../node.dart';
/// A value in a plain CSS tree.
///
/// This is used to associate a span with a value that doesn't otherwise track
-/// its span.
-class CssValue implements AstNode {
+/// its span. It has value equality semantics.
+final class CssValue implements AstNode {
/// The value.
final T value;
@@ -19,5 +19,10 @@ class CssValue implements AstNode {
CssValue(this.value, this.span);
+ bool operator ==(Object other) =>
+ other is CssValue && other.value == value;
+
+ int get hashCode => value.hashCode;
+
String toString() => value.toString();
}
diff --git a/lib/src/ast/node.dart b/lib/src/ast/node.dart
index d061a4787..e63fa9df7 100644
--- a/lib/src/ast/node.dart
+++ b/lib/src/ast/node.dart
@@ -14,7 +14,7 @@ import 'package:source_span/source_span.dart';
///
/// {@category AST}
@sealed
-abstract class AstNode {
+abstract interface class AstNode {
/// The source span associated with the node.
///
/// This indicates where in the source Sass or SCSS stylesheet the node was
diff --git a/lib/src/ast/sass/argument.dart b/lib/src/ast/sass/argument.dart
index 310154337..afd9e337c 100644
--- a/lib/src/ast/sass/argument.dart
+++ b/lib/src/ast/sass/argument.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../utils.dart';
@@ -14,8 +13,7 @@ import 'node.dart';
/// An argument declared as part of an [ArgumentDeclaration].
///
/// {@category AST}
-@sealed
-class Argument implements SassNode, SassDeclaration {
+final class Argument implements SassNode, SassDeclaration {
/// The argument name.
final String name;
diff --git a/lib/src/ast/sass/argument_declaration.dart b/lib/src/ast/sass/argument_declaration.dart
index 5e1ac5932..7ab73c3a5 100644
--- a/lib/src/ast/sass/argument_declaration.dart
+++ b/lib/src/ast/sass/argument_declaration.dart
@@ -2,15 +2,14 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../exception.dart';
import '../../logger.dart';
import '../../parse/scss.dart';
-import '../../utils.dart';
import '../../util/character.dart';
import '../../util/span.dart';
+import '../../utils.dart';
import 'argument.dart';
import 'node.dart';
@@ -18,8 +17,7 @@ import 'node.dart';
///
/// {@category AST}
/// {@category Parsing}
-@sealed
-class ArgumentDeclaration implements SassNode {
+final class ArgumentDeclaration implements SassNode {
/// The arguments that are taken.
final List arguments;
@@ -36,19 +34,19 @@ class ArgumentDeclaration implements SassNode {
// Move backwards through any whitespace between the name and the arguments.
var i = span.start.offset - 1;
- while (i > 0 && isWhitespace(text.codeUnitAt(i))) {
+ while (i > 0 && text.codeUnitAt(i).isWhitespace) {
i--;
}
// Then move backwards through the name itself.
- if (!isName(text.codeUnitAt(i))) return span;
+ if (!text.codeUnitAt(i).isName) return span;
i--;
- while (i >= 0 && isName(text.codeUnitAt(i))) {
+ while (i >= 0 && text.codeUnitAt(i).isName) {
i--;
}
// If the name didn't start with [isNameStart], it's not a valid identifier.
- if (!isNameStart(text.codeUnitAt(i + 1))) return span;
+ if (!text.codeUnitAt(i + 1).isNameStart) return span;
// Trim because it's possible that this span is empty (for example, a mixin
// may be declared without an argument list).
diff --git a/lib/src/ast/sass/argument_invocation.dart b/lib/src/ast/sass/argument_invocation.dart
index 25af571cd..92e7645cf 100644
--- a/lib/src/ast/sass/argument_invocation.dart
+++ b/lib/src/ast/sass/argument_invocation.dart
@@ -2,17 +2,18 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
+import '../../value/list.dart';
+import '../../util/map.dart';
import 'expression.dart';
+import 'expression/list.dart';
import 'node.dart';
/// A set of arguments passed in to a function or mixin.
///
/// {@category AST}
-@sealed
-class ArgumentInvocation implements SassNode {
+final class ArgumentInvocation implements SassNode {
/// The arguments passed by position.
final List positional;
@@ -47,11 +48,24 @@ class ArgumentInvocation implements SassNode {
String toString() {
var components = [
- ...positional,
- for (var name in named.keys) "\$$name: ${named[name]}",
- if (rest != null) "$rest...",
- if (keywordRest != null) "$keywordRest..."
+ for (var argument in positional) _parenthesizeArgument(argument),
+ for (var (name, value) in named.pairs)
+ "\$$name: ${_parenthesizeArgument(value)}",
+ if (rest case var rest?) "${_parenthesizeArgument(rest)}...",
+ if (keywordRest case var keywordRest?)
+ "${_parenthesizeArgument(keywordRest)}..."
];
return "(${components.join(', ')})";
}
+
+ /// Wraps [argument] in parentheses if necessary.
+ String _parenthesizeArgument(Expression argument) => switch (argument) {
+ ListExpression(
+ separator: ListSeparator.comma,
+ hasBrackets: false,
+ contents: [_, _, ...]
+ ) =>
+ "($argument)",
+ _ => argument.toString()
+ };
}
diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart
index c00665e4b..3bad9cf20 100644
--- a/lib/src/ast/sass/at_root_query.dart
+++ b/lib/src/ast/sass/at_root_query.dart
@@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import '../../exception.dart';
+import '../../interpolation_map.dart';
import '../../logger.dart';
import '../../parse/at_root_query.dart';
import '../css.dart';
@@ -13,8 +14,7 @@ import '../css.dart';
/// A query for the `@at-root` rule.
///
/// @nodoc
-@internal
-class AtRootQuery {
+final class AtRootQuery {
/// The default at-root query, which excludes only style rules.
static const defaultQuery = AtRootQuery._default();
@@ -53,8 +53,12 @@ class AtRootQuery {
///
/// If passed, [url] is the name of the file from which [contents] comes.
///
+ /// If passed, [interpolationMap] maps the text of [contents] back to the
+ /// original location of the selector in the source file.
+ ///
/// Throws a [SassFormatException] if parsing fails.
- factory AtRootQuery.parse(String contents, {Object? url, Logger? logger}) =>
+ factory AtRootQuery.parse(String contents,
+ {Object? url, Logger? logger, InterpolationMap? interpolationMap}) =>
AtRootQueryParser(contents, url: url, logger: logger).parse();
/// Returns whether [this] excludes [node].
@@ -63,11 +67,13 @@ class AtRootQuery {
@internal
bool excludes(CssParentNode node) {
if (_all) return !include;
- if (node is CssStyleRule) return excludesStyleRules;
- if (node is CssMediaRule) return excludesName("media");
- if (node is CssSupportsRule) return excludesName("supports");
- if (node is CssAtRule) return excludesName(node.name.value.toLowerCase());
- return false;
+ return switch (node) {
+ CssStyleRule() => excludesStyleRules,
+ CssMediaRule() => excludesName("media"),
+ CssSupportsRule() => excludesName("supports"),
+ CssAtRule() => excludesName(node.name.value.toLowerCase()),
+ _ => false
+ };
}
/// Returns whether [this] excludes an at-rule with the given [name].
diff --git a/lib/src/ast/sass/configured_variable.dart b/lib/src/ast/sass/configured_variable.dart
index 59bbc0287..c78066734 100644
--- a/lib/src/ast/sass/configured_variable.dart
+++ b/lib/src/ast/sass/configured_variable.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../util/span.dart';
@@ -13,8 +12,7 @@ import 'node.dart';
/// A variable configured by a `with` clause in a `@use` or `@forward` rule.
///
/// {@category AST}
-@sealed
-class ConfiguredVariable implements SassNode, SassDeclaration {
+final class ConfiguredVariable implements SassNode, SassDeclaration {
/// The name of the variable being configured.
final String name;
diff --git a/lib/src/ast/sass/declaration.dart b/lib/src/ast/sass/declaration.dart
index 0307e4994..0f2c78a41 100644
--- a/lib/src/ast/sass/declaration.dart
+++ b/lib/src/ast/sass/declaration.dart
@@ -11,7 +11,7 @@ import 'node.dart';
///
/// {@category AST}
@sealed
-abstract class SassDeclaration extends SassNode {
+abstract interface class SassDeclaration implements SassNode {
/// The name of the declaration, with underscores converted to hyphens.
///
/// This does not include the `$` for variables.
diff --git a/lib/src/ast/sass/dependency.dart b/lib/src/ast/sass/dependency.dart
index d48e27623..54172be7c 100644
--- a/lib/src/ast/sass/dependency.dart
+++ b/lib/src/ast/sass/dependency.dart
@@ -11,7 +11,7 @@ import 'node.dart';
///
/// {@category AST}
@sealed
-abstract class SassDependency extends SassNode {
+abstract interface class SassDependency implements SassNode {
/// The URL of the dependency this rule loads.
Uri get url;
diff --git a/lib/src/ast/sass/expression.dart b/lib/src/ast/sass/expression.dart
index 5a707bf7f..a5682411e 100644
--- a/lib/src/ast/sass/expression.dart
+++ b/lib/src/ast/sass/expression.dart
@@ -15,7 +15,7 @@ import 'node.dart';
/// {@category AST}
/// {@category Parsing}
@sealed
-abstract class Expression implements SassNode {
+abstract interface class Expression implements SassNode {
/// Calls the appropriate visit method on [visitor].
T accept(ExpressionVisitor visitor);
diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart
index d3b45d920..dfaf87d16 100644
--- a/lib/src/ast/sass/expression/binary_operation.dart
+++ b/lib/src/ast/sass/expression/binary_operation.dart
@@ -8,12 +8,12 @@ import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
+import 'list.dart';
/// A binary operator, as in `1 + 2` or `$this and $other`.
///
/// {@category AST}
-@sealed
-class BinaryOperationExpression implements Expression {
+final class BinaryOperationExpression implements Expression {
/// The operator being invoked.
final BinaryOperator operator;
@@ -63,9 +63,14 @@ class BinaryOperationExpression implements Expression {
String toString() {
var buffer = StringBuffer();
- var left = this.left; // Hack to make analysis work.
- var leftNeedsParens = left is BinaryOperationExpression &&
- left.operator.precedence < operator.precedence;
+ // dart-lang/language#3064 and #3062 track potential ways of making this
+ // cleaner.
+ var leftNeedsParens = switch (left) {
+ BinaryOperationExpression(operator: BinaryOperator(:var precedence)) =>
+ precedence < operator.precedence,
+ ListExpression(hasBrackets: false, contents: [_, _, ...]) => true,
+ _ => false
+ };
if (leftNeedsParens) buffer.writeCharCode($lparen);
buffer.write(left);
if (leftNeedsParens) buffer.writeCharCode($rparen);
@@ -75,8 +80,16 @@ class BinaryOperationExpression implements Expression {
buffer.writeCharCode($space);
var right = this.right; // Hack to make analysis work.
- var rightNeedsParens = right is BinaryOperationExpression &&
- right.operator.precedence <= operator.precedence;
+ var rightNeedsParens = switch (right) {
+ BinaryOperationExpression(:var operator) =>
+ // dart-lang/linter#4381
+ // ignore: unnecessary_this
+ operator.precedence <= this.operator.precedence &&
+ // ignore: unnecessary_this
+ !(operator == this.operator && operator.isAssociative),
+ ListExpression(hasBrackets: false, contents: [_, _, ...]) => true,
+ _ => false
+ };
if (rightNeedsParens) buffer.writeCharCode($lparen);
buffer.write(right);
if (rightNeedsParens) buffer.writeCharCode($rparen);
@@ -93,10 +106,10 @@ enum BinaryOperator {
singleEquals('single equals', '=', 0),
/// The disjunction operator, `or`.
- or('or', 'or', 1),
+ or('or', 'or', 1, associative: true),
/// The conjunction operator, `and`.
- and('and', 'and', 2),
+ and('and', 'and', 2, associative: true),
/// The equality operator, `==`.
equals('equals', '==', 3),
@@ -117,13 +130,13 @@ enum BinaryOperator {
lessThanOrEquals('less than or equals', '<=', 4),
/// The addition operator, `+`.
- plus('plus', '+', 5),
+ plus('plus', '+', 5, associative: true),
/// The subtraction operator, `-`.
minus('minus', '-', 5),
/// The multiplication operator, `*`.
- times('times', '*', 6),
+ times('times', '*', 6, associative: true),
/// The division operator, `/`.
dividedBy('divided by', '/', 6),
@@ -142,7 +155,14 @@ enum BinaryOperator {
/// An operator with higher precedence binds tighter.
final int precedence;
- const BinaryOperator(this.name, this.operator, this.precedence);
+ /// Whether this operation has the [associative property].
+ ///
+ /// [associative property]: https://en.wikipedia.org/wiki/Associative_property
+ final bool isAssociative;
+
+ const BinaryOperator(this.name, this.operator, this.precedence,
+ {bool associative = false})
+ : isAssociative = associative;
String toString() => name;
}
diff --git a/lib/src/ast/sass/expression/boolean.dart b/lib/src/ast/sass/expression/boolean.dart
index 0686d0bd0..23474a3f6 100644
--- a/lib/src/ast/sass/expression/boolean.dart
+++ b/lib/src/ast/sass/expression/boolean.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -11,8 +10,7 @@ import '../expression.dart';
/// A boolean literal, `true` or `false`.
///
/// {@category AST}
-@sealed
-class BooleanExpression implements Expression {
+final class BooleanExpression implements Expression {
/// The value of this expression.
final bool value;
diff --git a/lib/src/ast/sass/expression/calculation.dart b/lib/src/ast/sass/expression/calculation.dart
index ac17dc0aa..38c25ed14 100644
--- a/lib/src/ast/sass/expression/calculation.dart
+++ b/lib/src/ast/sass/expression/calculation.dart
@@ -18,8 +18,7 @@ import 'variable.dart';
/// A calculation literal.
///
/// {@category AST}
-@sealed
-class CalculationExpression implements Expression {
+final class CalculationExpression implements Expression {
/// This calculation's name.
final String name;
@@ -74,29 +73,31 @@ class CalculationExpression implements Expression {
/// Throws an [ArgumentError] if [expression] isn't a valid calculation
/// argument.
static void _verify(Expression expression) {
- if (expression is NumberExpression) return;
- if (expression is CalculationExpression) return;
- if (expression is VariableExpression) return;
- if (expression is FunctionExpression) return;
- if (expression is IfExpression) return;
-
- if (expression is StringExpression) {
- if (expression.hasQuotes) {
+ switch (expression) {
+ case NumberExpression() ||
+ CalculationExpression() ||
+ VariableExpression() ||
+ FunctionExpression() ||
+ IfExpression() ||
+ StringExpression(hasQuotes: false):
+ break;
+
+ case ParenthesizedExpression(:var expression):
+ _verify(expression);
+
+ case BinaryOperationExpression(
+ :var left,
+ :var right,
+ operator: BinaryOperator.plus ||
+ BinaryOperator.minus ||
+ BinaryOperator.times ||
+ BinaryOperator.dividedBy
+ ):
+ _verify(left);
+ _verify(right);
+
+ case _:
throw ArgumentError("Invalid calculation argument $expression.");
- }
- } else if (expression is ParenthesizedExpression) {
- _verify(expression.expression);
- } else if (expression is BinaryOperationExpression) {
- _verify(expression.left);
- _verify(expression.right);
- if (expression.operator == BinaryOperator.plus) return;
- if (expression.operator == BinaryOperator.minus) return;
- if (expression.operator == BinaryOperator.times) return;
- if (expression.operator == BinaryOperator.dividedBy) return;
-
- throw ArgumentError("Invalid calculation argument $expression.");
- } else {
- throw ArgumentError("Invalid calculation argument $expression.");
}
}
diff --git a/lib/src/ast/sass/expression/color.dart b/lib/src/ast/sass/expression/color.dart
index eef0e97f4..e81a7f8b8 100644
--- a/lib/src/ast/sass/expression/color.dart
+++ b/lib/src/ast/sass/expression/color.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../value.dart';
@@ -12,8 +11,7 @@ import '../expression.dart';
/// A color literal.
///
/// {@category AST}
-@sealed
-class ColorExpression implements Expression {
+final class ColorExpression implements Expression {
/// The value of this color.
final SassColor value;
diff --git a/lib/src/ast/sass/expression/function.dart b/lib/src/ast/sass/expression/function.dart
index 4d823ca2e..398a2ff03 100644
--- a/lib/src/ast/sass/expression/function.dart
+++ b/lib/src/ast/sass/expression/function.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -18,8 +17,7 @@ import '../reference.dart';
/// interpolation.
///
/// {@category AST}
-@sealed
-class FunctionExpression
+final class FunctionExpression
implements Expression, CallableInvocation, SassReference {
/// The namespace of the function being invoked, or `null` if it's invoked
/// without a namespace.
diff --git a/lib/src/ast/sass/expression/if.dart b/lib/src/ast/sass/expression/if.dart
index f41c4f8e9..8805d4bff 100644
--- a/lib/src/ast/sass/expression/if.dart
+++ b/lib/src/ast/sass/expression/if.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../ast/sass.dart';
@@ -15,8 +14,7 @@ import '../../../visitor/interface/expression.dart';
/// evaluated.
///
/// {@category AST}
-@sealed
-class IfExpression implements Expression, CallableInvocation {
+final class IfExpression implements Expression, CallableInvocation {
/// The declaration of `if()`, as though it were a normal function.
static final declaration = ArgumentDeclaration.parse(
r"@function if($condition, $if-true, $if-false) {");
diff --git a/lib/src/ast/sass/expression/interpolated_function.dart b/lib/src/ast/sass/expression/interpolated_function.dart
index ec1941f64..3c97b0c9f 100644
--- a/lib/src/ast/sass/expression/interpolated_function.dart
+++ b/lib/src/ast/sass/expression/interpolated_function.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -16,8 +15,8 @@ import '../interpolation.dart';
/// This is always a plain CSS function.
///
/// {@category AST}
-@sealed
-class InterpolatedFunctionExpression implements Expression, CallableInvocation {
+final class InterpolatedFunctionExpression
+ implements Expression, CallableInvocation {
/// The name of the function being invoked.
final Interpolation name;
diff --git a/lib/src/ast/sass/expression/list.dart b/lib/src/ast/sass/expression/list.dart
index 53a10f3fc..01416afa4 100644
--- a/lib/src/ast/sass/expression/list.dart
+++ b/lib/src/ast/sass/expression/list.dart
@@ -3,7 +3,6 @@
// https://opensource.org/licenses/MIT.
import 'package:charcode/charcode.dart';
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../value.dart';
@@ -14,8 +13,7 @@ import 'unary_operation.dart';
/// A list literal.
///
/// {@category AST}
-@sealed
-class ListExpression implements Expression {
+final class ListExpression implements Expression {
/// The elements of this list.
final List contents;
@@ -37,33 +35,44 @@ class ListExpression implements Expression {
String toString() {
var buffer = StringBuffer();
- if (hasBrackets) buffer.writeCharCode($lbracket);
+ if (hasBrackets) {
+ buffer.writeCharCode($lbracket);
+ } else if (contents.isEmpty ||
+ (contents.length == 1 && separator == ListSeparator.comma)) {
+ buffer.writeCharCode($lparen);
+ }
+
buffer.write(contents
.map((element) =>
_elementNeedsParens(element) ? "($element)" : element.toString())
.join(separator == ListSeparator.comma ? ", " : " "));
- if (hasBrackets) buffer.writeCharCode($rbracket);
+
+ if (hasBrackets) {
+ buffer.writeCharCode($rbracket);
+ } else if (contents.isEmpty) {
+ buffer.writeCharCode($rparen);
+ } else if (contents.length == 1 && separator == ListSeparator.comma) {
+ buffer.write(",)");
+ }
+
return buffer.toString();
}
/// Returns whether [expression], contained in [this], needs parentheses when
/// printed as Sass source.
- bool _elementNeedsParens(Expression expression) {
- if (expression is ListExpression) {
- if (expression.contents.length < 2) return false;
- if (expression.hasBrackets) return false;
- return separator == ListSeparator.comma
- ? expression.separator == ListSeparator.comma
- : expression.separator != ListSeparator.undecided;
- }
-
- if (separator != ListSeparator.space) return false;
-
- if (expression is UnaryOperationExpression) {
- return expression.operator == UnaryOperator.plus ||
- expression.operator == UnaryOperator.minus;
- }
-
- return false;
- }
+ bool _elementNeedsParens(Expression expression) => switch (expression) {
+ ListExpression(
+ contents: [_, _, ...],
+ hasBrackets: false,
+ separator: var childSeparator
+ ) =>
+ separator == ListSeparator.comma
+ ? childSeparator == ListSeparator.comma
+ : childSeparator != ListSeparator.undecided,
+ UnaryOperationExpression(
+ operator: UnaryOperator.plus || UnaryOperator.minus
+ ) =>
+ separator == ListSeparator.space,
+ _ => false
+ };
}
diff --git a/lib/src/ast/sass/expression/map.dart b/lib/src/ast/sass/expression/map.dart
index cabc4994f..9bc234780 100644
--- a/lib/src/ast/sass/expression/map.dart
+++ b/lib/src/ast/sass/expression/map.dart
@@ -2,9 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
-import 'package:tuple/tuple.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
@@ -12,21 +10,20 @@ import '../expression.dart';
/// A map literal.
///
/// {@category AST}
-@sealed
-class MapExpression implements Expression {
+final class MapExpression implements Expression {
/// The pairs in this map.
///
/// This is a list of pairs rather than a map because a map may have two keys
/// with the same expression (e.g. `(unique-id(): 1, unique-id(): 2)`).
- final List> pairs;
+ final List<(Expression, Expression)> pairs;
final FileSpan span;
- MapExpression(Iterable> pairs, this.span)
+ MapExpression(Iterable<(Expression, Expression)> pairs, this.span)
: pairs = List.unmodifiable(pairs);
T accept(ExpressionVisitor visitor) => visitor.visitMapExpression(this);
String toString() =>
- '(${pairs.map((pair) => '${pair.item1}: ${pair.item2}').join(', ')})';
+ '(${[for (var (key, value) in pairs) '$key: $value'].join(', ')})';
}
diff --git a/lib/src/ast/sass/expression/null.dart b/lib/src/ast/sass/expression/null.dart
index 0e236753e..4155c00b0 100644
--- a/lib/src/ast/sass/expression/null.dart
+++ b/lib/src/ast/sass/expression/null.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -11,8 +10,7 @@ import '../expression.dart';
/// A null literal.
///
/// {@category AST}
-@sealed
-class NullExpression implements Expression {
+final class NullExpression implements Expression {
final FileSpan span;
NullExpression(this.span);
diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart
index ad1f1ed1e..7eb2b6fd9 100644
--- a/lib/src/ast/sass/expression/number.dart
+++ b/lib/src/ast/sass/expression/number.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -12,8 +11,7 @@ import '../expression.dart';
/// A number literal.
///
/// {@category AST}
-@sealed
-class NumberExpression implements Expression {
+final class NumberExpression implements Expression {
/// The numeric value.
final double value;
diff --git a/lib/src/ast/sass/expression/parenthesized.dart b/lib/src/ast/sass/expression/parenthesized.dart
index 9b89731d3..3788645e3 100644
--- a/lib/src/ast/sass/expression/parenthesized.dart
+++ b/lib/src/ast/sass/expression/parenthesized.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -11,8 +10,7 @@ import '../expression.dart';
/// An expression wrapped in parentheses.
///
/// {@category AST}
-@sealed
-class ParenthesizedExpression implements Expression {
+final class ParenthesizedExpression implements Expression {
/// The internal expression.
final Expression expression;
diff --git a/lib/src/ast/sass/expression/selector.dart b/lib/src/ast/sass/expression/selector.dart
index c5209a88f..81356690b 100644
--- a/lib/src/ast/sass/expression/selector.dart
+++ b/lib/src/ast/sass/expression/selector.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -11,8 +10,7 @@ import '../expression.dart';
/// A parent selector reference, `&`.
///
/// {@category AST}
-@sealed
-class SelectorExpression implements Expression {
+final class SelectorExpression implements Expression {
final FileSpan span;
SelectorExpression(this.span);
diff --git a/lib/src/ast/sass/expression/string.dart b/lib/src/ast/sass/expression/string.dart
index 010907ff7..2e7824345 100644
--- a/lib/src/ast/sass/expression/string.dart
+++ b/lib/src/ast/sass/expression/string.dart
@@ -3,10 +3,11 @@
// https://opensource.org/licenses/MIT.
import 'package:charcode/charcode.dart';
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../interpolation_buffer.dart';
+// dart-lang/sdk#52535
+// ignore: unused_import
import '../../../util/character.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
@@ -15,8 +16,7 @@ import '../interpolation.dart';
/// A string literal.
///
/// {@category AST}
-@sealed
-class StringExpression implements Expression {
+final class StringExpression implements Expression {
/// Interpolation that, when evaluated, produces the contents of this string.
///
/// Unlike [asInterpolation], escapes are resolved and quotes are not
@@ -65,10 +65,11 @@ class StringExpression implements Expression {
buffer.writeCharCode(quote);
for (var value in text.contents) {
assert(value is Expression || value is String);
- if (value is Expression) {
- buffer.add(value);
- } else if (value is String) {
- _quoteInnerText(value, quote, buffer, static: static);
+ switch (value) {
+ case Expression():
+ buffer.add(value);
+ case String():
+ _quoteInnerText(value, quote, buffer, static: static);
}
}
buffer.writeCharCode(quote);
@@ -84,27 +85,28 @@ class StringExpression implements Expression {
static void _quoteInnerText(String text, int quote, StringSink buffer,
{bool static = false}) {
for (var i = 0; i < text.length; i++) {
- var codeUnit = text.codeUnitAt(i);
-
- if (isNewline(codeUnit)) {
- buffer.writeCharCode($backslash);
- buffer.writeCharCode($a);
- if (i != text.length - 1) {
- var next = text.codeUnitAt(i + 1);
- if (isWhitespace(next) || isHex(next)) {
- buffer.writeCharCode($space);
+ switch (text.codeUnitAt(i)) {
+ case int(isNewline: true):
+ buffer.writeCharCode($backslash);
+ buffer.writeCharCode($a);
+ if (i != text.length - 1) {
+ if (text.codeUnitAt(i + 1)
+ case int(isWhitespace: true) || int(isHex: true)) {
+ buffer.writeCharCode($space);
+ }
}
- }
- } else {
- if (codeUnit == quote ||
- codeUnit == $backslash ||
- (static &&
- codeUnit == $hash &&
+
+ case $backslash && var codeUnit:
+ case var codeUnit when codeUnit == quote:
+ case $hash && var codeUnit
+ when static &&
i < text.length - 1 &&
- text.codeUnitAt(i + 1) == $lbrace)) {
+ text.codeUnitAt(i + 1) == $lbrace:
buffer.writeCharCode($backslash);
- }
- buffer.writeCharCode(codeUnit);
+ buffer.writeCharCode(codeUnit);
+
+ case var codeUnit:
+ buffer.writeCharCode(codeUnit);
}
}
}
@@ -114,8 +116,7 @@ class StringExpression implements Expression {
static int _bestQuote(Iterable strings) {
var containsDoubleQuote = false;
for (var value in strings) {
- for (var i = 0; i < value.length; i++) {
- var codeUnit = value.codeUnitAt(i);
+ for (var codeUnit in value.codeUnits) {
if (codeUnit == $single_quote) return $double_quote;
if (codeUnit == $double_quote) containsDoubleQuote = true;
}
diff --git a/lib/src/ast/sass/expression/supports.dart b/lib/src/ast/sass/expression/supports.dart
index 3aa28222c..d5de09a75 100644
--- a/lib/src/ast/sass/expression/supports.dart
+++ b/lib/src/ast/sass/expression/supports.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -15,8 +14,7 @@ import '../supports_condition.dart';
/// doesn't include the function name wrapping the condition.
///
/// {@category AST}
-@sealed
-class SupportsExpression implements Expression {
+final class SupportsExpression implements Expression {
/// The condition itself.
final SupportsCondition condition;
diff --git a/lib/src/ast/sass/expression/unary_operation.dart b/lib/src/ast/sass/expression/unary_operation.dart
index 201e1a821..d437fafc2 100644
--- a/lib/src/ast/sass/expression/unary_operation.dart
+++ b/lib/src/ast/sass/expression/unary_operation.dart
@@ -3,17 +3,17 @@
// https://opensource.org/licenses/MIT.
import 'package:charcode/charcode.dart';
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
+import 'binary_operation.dart';
+import 'list.dart';
/// A unary operator, as in `+$var` or `not fn()`.
///
/// {@category AST}
-@sealed
-class UnaryOperationExpression implements Expression {
+final class UnaryOperationExpression implements Expression {
/// The operator being invoked.
final UnaryOperator operator;
@@ -30,7 +30,17 @@ class UnaryOperationExpression implements Expression {
String toString() {
var buffer = StringBuffer(operator.operator);
if (operator == UnaryOperator.not) buffer.writeCharCode($space);
+ var operand = this.operand;
+ var needsParens = switch (operand) {
+ BinaryOperationExpression() ||
+ UnaryOperationExpression() ||
+ ListExpression(hasBrackets: false, contents: [_, _, ...]) =>
+ true,
+ _ => false
+ };
+ if (needsParens) buffer.write($lparen);
buffer.write(operand);
+ if (needsParens) buffer.write($rparen);
return buffer.toString();
}
}
diff --git a/lib/src/ast/sass/expression/value.dart b/lib/src/ast/sass/expression/value.dart
index 55fd52390..75b01212e 100644
--- a/lib/src/ast/sass/expression/value.dart
+++ b/lib/src/ast/sass/expression/value.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
@@ -15,8 +14,7 @@ import '../expression.dart';
/// constructed dynamically, as for the `call()` function.
///
/// {@category AST}
-@sealed
-class ValueExpression implements Expression {
+final class ValueExpression implements Expression {
/// The embedded value.
final Value value;
diff --git a/lib/src/ast/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart
index 2c5eace35..c07ffbc5a 100644
--- a/lib/src/ast/sass/expression/variable.dart
+++ b/lib/src/ast/sass/expression/variable.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -13,8 +12,7 @@ import '../reference.dart';
/// A Sass variable.
///
/// {@category AST}
-@sealed
-class VariableExpression implements Expression, SassReference {
+final class VariableExpression implements Expression, SassReference {
/// The namespace of the variable being referenced, or `null` if it's
/// referenced without a namespace.
final String? namespace;
diff --git a/lib/src/ast/sass/import.dart b/lib/src/ast/sass/import.dart
index 3747a1a3b..7022f3b73 100644
--- a/lib/src/ast/sass/import.dart
+++ b/lib/src/ast/sass/import.dart
@@ -2,12 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
-
import 'node.dart';
/// An abstract superclass for different types of import.
///
/// {@category AST}
-@sealed
-abstract class Import implements SassNode {}
+abstract interface class Import implements SassNode {}
diff --git a/lib/src/ast/sass/import/dynamic.dart b/lib/src/ast/sass/import/dynamic.dart
index bb45cc6b0..38bb6c7f0 100644
--- a/lib/src/ast/sass/import/dynamic.dart
+++ b/lib/src/ast/sass/import/dynamic.dart
@@ -12,8 +12,7 @@ import '../import.dart';
/// An import that will load a Sass file at runtime.
///
/// {@category AST}
-@sealed
-class DynamicImport implements Import, SassDependency {
+final class DynamicImport implements Import, SassDependency {
/// The URL of the file to import.
///
/// If this is relative, it's relative to the containing file.
diff --git a/lib/src/ast/sass/import/static.dart b/lib/src/ast/sass/import/static.dart
index 69b1e3bb3..e20ee0d3f 100644
--- a/lib/src/ast/sass/import/static.dart
+++ b/lib/src/ast/sass/import/static.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../import.dart';
@@ -11,8 +10,7 @@ import '../interpolation.dart';
/// An import that produces a plain CSS `@import` rule.
///
/// {@category AST}
-@sealed
-class StaticImport implements Import {
+final class StaticImport implements Import {
/// The URL for this import.
///
/// This already contains quotes.
diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart
index d3a55971a..578394e83 100644
--- a/lib/src/ast/sass/interpolation.dart
+++ b/lib/src/ast/sass/interpolation.dart
@@ -12,8 +12,7 @@ import 'node.dart';
/// Plain text interpolated with Sass expressions.
///
/// {@category AST}
-@sealed
-class Interpolation implements SassNode {
+final class Interpolation implements SassNode {
/// The contents of this interpolation.
///
/// This contains [String]s and [Expression]s. It never contains two adjacent
@@ -25,21 +24,15 @@ class Interpolation implements SassNode {
/// If this contains no interpolated expressions, returns its text contents.
///
/// Otherwise, returns `null`.
- String? get asPlain {
- if (contents.isEmpty) return '';
- if (contents.length > 1) return null;
- var first = contents.first;
- return first is String ? first : null;
- }
+ String? get asPlain =>
+ switch (contents) { [] => '', [String first] => first, _ => null };
/// Returns the plain text before the interpolation, or the empty string.
///
/// @nodoc
@internal
- String get initialPlain {
- var first = contents.first;
- return first is String ? first : '';
- }
+ String get initialPlain =>
+ switch (contents) { [String first, ...] => first, _ => '' };
/// Creates a new [Interpolation] by concatenating a sequence of [String]s,
/// [Expression]s, or nested [Interpolation]s.
@@ -48,15 +41,16 @@ class Interpolation implements SassNode {
FileSpan span) {
var buffer = InterpolationBuffer();
for (var element in contents) {
- if (element is String) {
- buffer.write(element);
- } else if (element is Expression) {
- buffer.add(element);
- } else if (element is Interpolation) {
- buffer.addInterpolation(element);
- } else {
- throw ArgumentError.value(contents, "contents",
- "May only contains Strings, Expressions, or Interpolations.");
+ switch (element) {
+ case String():
+ buffer.write(element);
+ case Expression():
+ buffer.add(element);
+ case Interpolation():
+ buffer.addInterpolation(element);
+ case _:
+ throw ArgumentError.value(contents, "contents",
+ "May only contains Strings, Expressions, or Interpolations.");
}
}
diff --git a/lib/src/ast/sass/node.dart b/lib/src/ast/sass/node.dart
index 4b8d28b08..ee5211a31 100644
--- a/lib/src/ast/sass/node.dart
+++ b/lib/src/ast/sass/node.dart
@@ -10,4 +10,4 @@ import '../node.dart';
///
/// {@category AST}
@sealed
-abstract class SassNode extends AstNode {}
+abstract interface class SassNode implements AstNode {}
diff --git a/lib/src/ast/sass/reference.dart b/lib/src/ast/sass/reference.dart
index ffa838a00..06ed05b73 100644
--- a/lib/src/ast/sass/reference.dart
+++ b/lib/src/ast/sass/reference.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import 'node.dart';
@@ -10,8 +9,7 @@ import 'node.dart';
/// A common interface for any node that references a Sass member.
///
/// {@category AST}
-@sealed
-abstract class SassReference extends SassNode {
+abstract interface class SassReference implements SassNode {
/// The namespace of the member being referenced, or `null` if it's referenced
/// without a namespace.
String? get namespace;
diff --git a/lib/src/ast/sass/statement.dart b/lib/src/ast/sass/statement.dart
index 57a2861b5..123cf3362 100644
--- a/lib/src/ast/sass/statement.dart
+++ b/lib/src/ast/sass/statement.dart
@@ -2,16 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
-
import '../../visitor/interface/statement.dart';
import 'node.dart';
/// A statement in a Sass syntax tree.
///
/// {@category AST}
-@sealed
-abstract class Statement implements SassNode {
+abstract interface class Statement implements SassNode {
/// Calls the appropriate visit method on [visitor].
T accept(StatementVisitor visitor);
}
diff --git a/lib/src/ast/sass/statement/at_root_rule.dart b/lib/src/ast/sass/statement/at_root_rule.dart
index 51b4f7c88..a354d4794 100644
--- a/lib/src/ast/sass/statement/at_root_rule.dart
+++ b/lib/src/ast/sass/statement/at_root_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -15,8 +14,7 @@ import 'parent.dart';
/// This moves it contents "up" the tree through parent nodes.
///
/// {@category AST}
-@sealed
-class AtRootRule extends ParentStatement> {
+final class AtRootRule extends ParentStatement> {
/// The query specifying which statements this should move its contents
/// through.
final Interpolation? query;
diff --git a/lib/src/ast/sass/statement/at_rule.dart b/lib/src/ast/sass/statement/at_rule.dart
index e896c82b6..48c1629f9 100644
--- a/lib/src/ast/sass/statement/at_rule.dart
+++ b/lib/src/ast/sass/statement/at_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -13,8 +12,7 @@ import 'parent.dart';
/// An unknown at-rule.
///
/// {@category AST}
-@sealed
-class AtRule extends ParentStatement {
+final class AtRule extends ParentStatement {
/// The name of this rule.
final Interpolation name;
diff --git a/lib/src/ast/sass/statement/callable_declaration.dart b/lib/src/ast/sass/statement/callable_declaration.dart
index 7f7fec48a..e39b9c035 100644
--- a/lib/src/ast/sass/statement/callable_declaration.dart
+++ b/lib/src/ast/sass/statement/callable_declaration.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../argument_declaration.dart';
@@ -14,8 +13,8 @@ import 'silent_comment.dart';
/// user code.
///
/// {@category AST}
-@sealed
-abstract class CallableDeclaration extends ParentStatement> {
+abstract base class CallableDeclaration
+ extends ParentStatement> {
/// The name of this callable, with underscores converted to hyphens.
final String name;
diff --git a/lib/src/ast/sass/statement/content_block.dart b/lib/src/ast/sass/statement/content_block.dart
index 99af7746f..618a49ea5 100644
--- a/lib/src/ast/sass/statement/content_block.dart
+++ b/lib/src/ast/sass/statement/content_block.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -13,8 +12,7 @@ import 'callable_declaration.dart';
/// An anonymous block of code that's invoked for a [ContentRule].
///
/// {@category AST}
-@sealed
-class ContentBlock extends CallableDeclaration {
+final class ContentBlock extends CallableDeclaration {
ContentBlock(ArgumentDeclaration arguments, Iterable children,
FileSpan span)
: super("@content", arguments, children, span);
diff --git a/lib/src/ast/sass/statement/content_rule.dart b/lib/src/ast/sass/statement/content_rule.dart
index d2dcb6914..a05066ef0 100644
--- a/lib/src/ast/sass/statement/content_rule.dart
+++ b/lib/src/ast/sass/statement/content_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -15,8 +14,7 @@ import '../statement.dart';
/// caller.
///
/// {@category AST}
-@sealed
-class ContentRule implements Statement {
+final class ContentRule implements Statement {
/// The arguments pass to this `@content` rule.
///
/// This will be an empty invocation if `@content` has no arguments.
diff --git a/lib/src/ast/sass/statement/debug_rule.dart b/lib/src/ast/sass/statement/debug_rule.dart
index 63601cf44..47c2d452d 100644
--- a/lib/src/ast/sass/statement/debug_rule.dart
+++ b/lib/src/ast/sass/statement/debug_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -14,8 +13,7 @@ import '../statement.dart';
/// This prints a Sass value for debugging purposes.
///
/// {@category AST}
-@sealed
-class DebugRule implements Statement {
+final class DebugRule implements Statement {
/// The expression to print.
final Expression expression;
diff --git a/lib/src/ast/sass/statement/declaration.dart b/lib/src/ast/sass/statement/declaration.dart
index 1f55e22a2..852bc3145 100644
--- a/lib/src/ast/sass/statement/declaration.dart
+++ b/lib/src/ast/sass/statement/declaration.dart
@@ -16,8 +16,7 @@ import 'parent.dart';
/// A declaration (that is, a `name: value` pair).
///
/// {@category AST}
-@sealed
-class Declaration extends ParentStatement {
+final class Declaration extends ParentStatement {
/// The name of this declaration.
final Interpolation name;
@@ -42,25 +41,14 @@ class Declaration extends ParentStatement {
bool get isCustomProperty => name.initialPlain.startsWith('--');
/// Creates a declaration with no children.
- Declaration(this.name, this.value, this.span) : super(null) {
- if (isCustomProperty && value is! StringExpression) {
- throw ArgumentError(
- 'Declarations whose names begin with "--" must have StringExpression '
- 'values (was `$value` of type ${value.runtimeType}).');
- }
- }
+ Declaration(this.name, this.value, this.span) : super(null);
/// Creates a declaration with children.
///
/// For these declarations, a value is optional.
Declaration.nested(this.name, Iterable children, this.span,
{this.value})
- : super(List.unmodifiable(children)) {
- if (isCustomProperty && value is! StringExpression) {
- throw ArgumentError(
- 'Declarations whose names begin with "--" may not be nested.');
- }
- }
+ : super(List.unmodifiable(children));
T accept(StatementVisitor visitor) => visitor.visitDeclaration(this);
@@ -70,13 +58,14 @@ class Declaration extends ParentStatement {
buffer.writeCharCode($colon);
if (value != null) {
- if (!isCustomProperty) {
- buffer.writeCharCode($space);
- }
+ if (!isCustomProperty) buffer.writeCharCode($space);
buffer.write("$value");
}
- var children = this.children;
- return children == null ? "$buffer;" : "$buffer {${children.join(" ")}}";
+ if (children case var children?) {
+ return "$buffer {${children.join(" ")}}";
+ } else {
+ return "$buffer;";
+ }
}
}
diff --git a/lib/src/ast/sass/statement/each_rule.dart b/lib/src/ast/sass/statement/each_rule.dart
index bcdd37c07..68500ef56 100644
--- a/lib/src/ast/sass/statement/each_rule.dart
+++ b/lib/src/ast/sass/statement/each_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -15,8 +14,7 @@ import 'parent.dart';
/// This iterates over values in a list or map.
///
/// {@category AST}
-@sealed
-class EachRule extends ParentStatement> {
+final class EachRule extends ParentStatement> {
/// The variables assigned for each iteration.
final List variables;
diff --git a/lib/src/ast/sass/statement/error_rule.dart b/lib/src/ast/sass/statement/error_rule.dart
index a82e404f4..977567cbd 100644
--- a/lib/src/ast/sass/statement/error_rule.dart
+++ b/lib/src/ast/sass/statement/error_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -14,8 +13,7 @@ import '../statement.dart';
/// This emits an error and stops execution.
///
/// {@category AST}
-@sealed
-class ErrorRule implements Statement {
+final class ErrorRule implements Statement {
/// The expression to evaluate for the error message.
final Expression expression;
diff --git a/lib/src/ast/sass/statement/extend_rule.dart b/lib/src/ast/sass/statement/extend_rule.dart
index 7ba9f0c8c..8aa4e4e33 100644
--- a/lib/src/ast/sass/statement/extend_rule.dart
+++ b/lib/src/ast/sass/statement/extend_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -14,8 +13,7 @@ import '../statement.dart';
/// This gives one selector all the styling of another.
///
/// {@category AST}
-@sealed
-class ExtendRule implements Statement {
+final class ExtendRule implements Statement {
/// The interpolation for the selector that will be extended.
final Interpolation selector;
diff --git a/lib/src/ast/sass/statement/for_rule.dart b/lib/src/ast/sass/statement/for_rule.dart
index 8aef52e51..008f4d1f2 100644
--- a/lib/src/ast/sass/statement/for_rule.dart
+++ b/lib/src/ast/sass/statement/for_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -15,8 +14,7 @@ import 'parent.dart';
/// This iterates a set number of times.
///
/// {@category AST}
-@sealed
-class ForRule extends ParentStatement> {
+final class ForRule extends ParentStatement> {
/// The name of the variable that will contain the index value.
final String variable;
diff --git a/lib/src/ast/sass/statement/forward_rule.dart b/lib/src/ast/sass/statement/forward_rule.dart
index 499f502b7..eea2a226d 100644
--- a/lib/src/ast/sass/statement/forward_rule.dart
+++ b/lib/src/ast/sass/statement/forward_rule.dart
@@ -3,7 +3,6 @@
// https://opensource.org/licenses/MIT.
import 'package:collection/collection.dart';
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -16,8 +15,7 @@ import '../statement.dart';
/// A `@forward` rule.
///
/// {@category AST}
-@sealed
-class ForwardRule implements Statement, SassDependency {
+final class ForwardRule implements Statement, SassDependency {
/// The URI of the module to forward.
///
/// If this is relative, it's relative to the containing file.
diff --git a/lib/src/ast/sass/statement/function_rule.dart b/lib/src/ast/sass/statement/function_rule.dart
index 0b2327e52..9242bf858 100644
--- a/lib/src/ast/sass/statement/function_rule.dart
+++ b/lib/src/ast/sass/statement/function_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -18,8 +17,8 @@ import 'silent_comment.dart';
/// This declares a function that's invoked using normal CSS function syntax.
///
/// {@category AST}
-@sealed
-class FunctionRule extends CallableDeclaration implements SassDeclaration {
+final class FunctionRule extends CallableDeclaration
+ implements SassDeclaration {
FileSpan get nameSpan => span.withoutInitialAtRule().initialIdentifier();
FunctionRule(String name, ArgumentDeclaration arguments,
diff --git a/lib/src/ast/sass/statement/if_rule.dart b/lib/src/ast/sass/statement/if_rule.dart
index 056cbd9f8..2a92ac28c 100644
--- a/lib/src/ast/sass/statement/if_rule.dart
+++ b/lib/src/ast/sass/statement/if_rule.dart
@@ -20,8 +20,7 @@ import 'variable_declaration.dart';
/// This conditionally executes a block of code.
///
/// {@category AST}
-@sealed
-class IfRule implements Statement {
+final class IfRule implements Statement {
/// The `@if` and `@else if` clauses.
///
/// The first clause whose expression evaluates to `true` will have its
@@ -44,7 +43,8 @@ class IfRule implements Statement {
String toString() {
var result = clauses
.mapIndexed((index, clause) =>
- "@${index == 0 ? 'if' : 'else if'} ${clause.expression} {${clause.children.join(' ')}}")
+ "@${index == 0 ? 'if' : 'else if'} ${clause.expression} "
+ "{${clause.children.join(' ')}}")
.join(' ');
var lastClause = this.lastClause;
@@ -56,8 +56,7 @@ class IfRule implements Statement {
/// The superclass of `@if` and `@else` clauses.
///
/// {@category AST}
-@sealed
-abstract class IfRuleClause {
+sealed class IfRuleClause {
/// The statements to evaluate if this clause matches.
final List children;
@@ -71,19 +70,18 @@ abstract class IfRuleClause {
: this._(List.unmodifiable(children));
IfRuleClause._(this.children)
- : hasDeclarations = children.any((child) =>
- child is VariableDeclaration ||
- child is FunctionRule ||
- child is MixinRule ||
- (child is ImportRule &&
- child.imports.any((import) => import is DynamicImport)));
+ : hasDeclarations = children.any((child) => switch (child) {
+ VariableDeclaration() || FunctionRule() || MixinRule() => true,
+ ImportRule(:var imports) =>
+ imports.any((import) => import is DynamicImport),
+ _ => false
+ });
}
/// An `@if` or `@else if` clause in an `@if` rule.
///
/// {@category AST}
-@sealed
-class IfClause extends IfRuleClause {
+final class IfClause extends IfRuleClause {
/// The expression to evaluate to determine whether to run this rule.
final Expression expression;
@@ -95,8 +93,7 @@ class IfClause extends IfRuleClause {
/// An `@else` clause in an `@if` rule.
///
/// {@category AST}
-@sealed
-class ElseClause extends IfRuleClause {
+final class ElseClause extends IfRuleClause {
ElseClause(Iterable children) : super(children);
String toString() => "@else {${children.join(' ')}}";
diff --git a/lib/src/ast/sass/statement/import_rule.dart b/lib/src/ast/sass/statement/import_rule.dart
index c18f8e12e..425c3ac42 100644
--- a/lib/src/ast/sass/statement/import_rule.dart
+++ b/lib/src/ast/sass/statement/import_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -12,8 +11,7 @@ import '../statement.dart';
/// An `@import` rule.
///
/// {@category AST}
-@sealed
-class ImportRule implements Statement {
+final class ImportRule implements Statement {
/// The imports imported by this statement.
final List imports;
diff --git a/lib/src/ast/sass/statement/include_rule.dart b/lib/src/ast/sass/statement/include_rule.dart
index 263675284..d3c9ceba6 100644
--- a/lib/src/ast/sass/statement/include_rule.dart
+++ b/lib/src/ast/sass/statement/include_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -16,8 +15,8 @@ import 'content_block.dart';
/// A mixin invocation.
///
/// {@category AST}
-@sealed
-class IncludeRule implements Statement, CallableInvocation, SassReference {
+final class IncludeRule
+ implements Statement, CallableInvocation, SassReference {
/// The namespace of the mixin being invoked, or `null` if it's invoked
/// without a namespace.
final String? namespace;
diff --git a/lib/src/ast/sass/statement/loud_comment.dart b/lib/src/ast/sass/statement/loud_comment.dart
index 2876f1ad1..0c48e09fc 100644
--- a/lib/src/ast/sass/statement/loud_comment.dart
+++ b/lib/src/ast/sass/statement/loud_comment.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -12,8 +11,7 @@ import '../statement.dart';
/// A loud CSS-style comment.
///
/// {@category AST}
-@sealed
-class LoudComment implements Statement {
+final class LoudComment implements Statement {
/// The interpolated text of this comment, including comment characters.
final Interpolation text;
diff --git a/lib/src/ast/sass/statement/media_rule.dart b/lib/src/ast/sass/statement/media_rule.dart
index ec63111c7..d219ca007 100644
--- a/lib/src/ast/sass/statement/media_rule.dart
+++ b/lib/src/ast/sass/statement/media_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -13,8 +12,7 @@ import 'parent.dart';
/// A `@media` rule.
///
/// {@category AST}
-@sealed
-class MediaRule extends ParentStatement> {
+final class MediaRule extends ParentStatement> {
/// The query that determines on which platforms the styles will be in effect.
///
/// This is only parsed after the interpolation has been resolved.
diff --git a/lib/src/ast/sass/statement/mixin_rule.dart b/lib/src/ast/sass/statement/mixin_rule.dart
index 20e2ac254..624eff53e 100644
--- a/lib/src/ast/sass/statement/mixin_rule.dart
+++ b/lib/src/ast/sass/statement/mixin_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
@@ -20,8 +19,7 @@ import 'silent_comment.dart';
/// This declares a mixin that's invoked using `@include`.
///
/// {@category AST}
-@sealed
-class MixinRule extends CallableDeclaration implements SassDeclaration {
+final class MixinRule extends CallableDeclaration implements SassDeclaration {
/// Whether the mixin contains a `@content` rule.
late final bool hasContent =
const _HasContentVisitor().visitMixinRule(this) == true;
diff --git a/lib/src/ast/sass/statement/parent.dart b/lib/src/ast/sass/statement/parent.dart
index 329e45ba7..21293019d 100644
--- a/lib/src/ast/sass/statement/parent.dart
+++ b/lib/src/ast/sass/statement/parent.dart
@@ -17,8 +17,7 @@ import 'variable_declaration.dart';
/// not their children lists are nullable.
///
/// {@category AST}
-@sealed
-abstract class ParentStatement?>
+abstract base class ParentStatement?>
implements Statement {
/// The child statements of this statement.
final T children;
@@ -31,11 +30,14 @@ abstract class ParentStatement?>
final bool hasDeclarations;
ParentStatement(this.children)
- : hasDeclarations = children?.any((child) =>
- child is VariableDeclaration ||
- child is FunctionRule ||
- child is MixinRule ||
- (child is ImportRule &&
- child.imports.any((import) => import is DynamicImport))) ??
+ : hasDeclarations = children?.any((child) => switch (child) {
+ VariableDeclaration() ||
+ FunctionRule() ||
+ MixinRule() =>
+ true,
+ ImportRule(:var imports) =>
+ imports.any((import) => import is DynamicImport),
+ _ => false,
+ }) ??
false;
}
diff --git a/lib/src/ast/sass/statement/return_rule.dart b/lib/src/ast/sass/statement/return_rule.dart
index bbf7fd370..dc1efc65c 100644
--- a/lib/src/ast/sass/statement/return_rule.dart
+++ b/lib/src/ast/sass/statement/return_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -14,8 +13,7 @@ import '../statement.dart';
/// This exits from the current function body with a return value.
///
/// {@category AST}
-@sealed
-class ReturnRule implements Statement {
+final class ReturnRule implements Statement {
/// The value to return from this function.
final Expression expression;
diff --git a/lib/src/ast/sass/statement/silent_comment.dart b/lib/src/ast/sass/statement/silent_comment.dart
index 189c5d79f..384cd09fb 100644
--- a/lib/src/ast/sass/statement/silent_comment.dart
+++ b/lib/src/ast/sass/statement/silent_comment.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
@@ -12,8 +11,7 @@ import '../statement.dart';
/// A silent Sass-style comment.
///
/// {@category AST}
-@sealed
-class SilentComment implements Statement {
+final class SilentComment implements Statement {
/// The text of this comment, including comment characters.
final String text;
diff --git a/lib/src/ast/sass/statement/style_rule.dart b/lib/src/ast/sass/statement/style_rule.dart
index 02b6c8ea9..32031c762 100644
--- a/lib/src/ast/sass/statement/style_rule.dart
+++ b/lib/src/ast/sass/statement/style_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -15,8 +14,7 @@ import 'parent.dart';
/// This applies style declarations to elements that match a given selector.
///
/// {@category AST}
-@sealed
-class StyleRule extends ParentStatement> {
+final class StyleRule extends ParentStatement> {
/// The selector to which the declaration will be applied.
///
/// This is only parsed after the interpolation has been resolved.
diff --git a/lib/src/ast/sass/statement/stylesheet.dart b/lib/src/ast/sass/statement/stylesheet.dart
index b0acd1218..b90cddc52 100644
--- a/lib/src/ast/sass/statement/stylesheet.dart
+++ b/lib/src/ast/sass/statement/stylesheet.dart
@@ -13,6 +13,7 @@ import '../../../parse/css.dart';
import '../../../parse/sass.dart';
import '../../../parse/scss.dart';
import '../../../syntax.dart';
+import '../../../utils.dart';
import '../../../visitor/interface/statement.dart';
import '../statement.dart';
import 'forward_rule.dart';
@@ -28,8 +29,7 @@ import 'variable_declaration.dart';
///
/// {@category AST}
/// {@category Parsing}
-@sealed
-class Stylesheet extends ParentStatement> {
+final class Stylesheet extends ParentStatement> {
final FileSpan span;
/// Whether this was parsed from a plain CSS stylesheet.
@@ -56,15 +56,22 @@ class Stylesheet extends ParentStatement> {
Stylesheet.internal(Iterable children, this.span,
{this.plainCss = false})
: super(List.unmodifiable(children)) {
+ loop:
for (var child in this.children) {
- if (child is UseRule) {
- _uses.add(child);
- } else if (child is ForwardRule) {
- _forwards.add(child);
- } else if (child is! SilentComment &&
- child is! LoudComment &&
- child is! VariableDeclaration) {
- break;
+ switch (child) {
+ case UseRule():
+ _uses.add(child);
+
+ case ForwardRule():
+ _forwards.add(child);
+
+ case SilentComment() || LoudComment() || VariableDeclaration():
+ // These are allowed between `@use` and `@forward` rules.
+ break;
+
+ case _:
+ break loop;
+ // Once we reach anything else, we know we're done with loads.
}
}
}
@@ -76,15 +83,23 @@ class Stylesheet extends ParentStatement> {
/// Throws a [SassFormatException] if parsing fails.
factory Stylesheet.parse(String contents, Syntax syntax,
{Object? url, Logger? logger}) {
- switch (syntax) {
- case Syntax.sass:
- return Stylesheet.parseSass(contents, url: url, logger: logger);
- case Syntax.scss:
- return Stylesheet.parseScss(contents, url: url, logger: logger);
- case Syntax.css:
- return Stylesheet.parseCss(contents, url: url, logger: logger);
- default:
- throw ArgumentError("Unknown syntax $syntax.");
+ try {
+ switch (syntax) {
+ case Syntax.sass:
+ return Stylesheet.parseSass(contents, url: url, logger: logger);
+ case Syntax.scss:
+ return Stylesheet.parseScss(contents, url: url, logger: logger);
+ case Syntax.css:
+ return Stylesheet.parseCss(contents, url: url, logger: logger);
+ default:
+ throw ArgumentError("Unknown syntax $syntax.");
+ }
+ } on SassException catch (error, stackTrace) {
+ var url = error.span.sourceUrl;
+ if (url == null || url.toString() == 'stdin') rethrow;
+
+ throw throwWithTrace(
+ error.withLoadedUrls(Set.unmodifiable({url})), error, stackTrace);
}
}
diff --git a/lib/src/ast/sass/statement/supports_rule.dart b/lib/src/ast/sass/statement/supports_rule.dart
index fe23d97ad..13bba084f 100644
--- a/lib/src/ast/sass/statement/supports_rule.dart
+++ b/lib/src/ast/sass/statement/supports_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -13,8 +12,7 @@ import 'parent.dart';
/// A `@supports` rule.
///
/// {@category AST}
-@sealed
-class SupportsRule extends ParentStatement> {
+final class SupportsRule extends ParentStatement> {
/// The condition that selects what browsers this rule targets.
final SupportsCondition condition;
diff --git a/lib/src/ast/sass/statement/use_rule.dart b/lib/src/ast/sass/statement/use_rule.dart
index 0e84759ad..244613abc 100644
--- a/lib/src/ast/sass/statement/use_rule.dart
+++ b/lib/src/ast/sass/statement/use_rule.dart
@@ -18,8 +18,7 @@ import '../statement.dart';
/// A `@use` rule.
///
/// {@category AST}
-@sealed
-class UseRule implements Statement, SassDependency {
+final class UseRule implements Statement, SassDependency {
/// The URI of the module to use.
///
/// If this is relative, it's relative to the containing file.
diff --git a/lib/src/ast/sass/statement/variable_declaration.dart b/lib/src/ast/sass/statement/variable_declaration.dart
index a82401b0a..235a41648 100644
--- a/lib/src/ast/sass/statement/variable_declaration.dart
+++ b/lib/src/ast/sass/statement/variable_declaration.dart
@@ -21,8 +21,7 @@ import 'silent_comment.dart';
/// This defines or sets a variable.
///
/// {@category AST}
-@sealed
-class VariableDeclaration implements Statement, SassDeclaration {
+final class VariableDeclaration implements Statement, SassDeclaration {
/// The namespace of the variable being set, or `null` if it's defined or set
/// without a namespace.
final String? namespace;
diff --git a/lib/src/ast/sass/statement/warn_rule.dart b/lib/src/ast/sass/statement/warn_rule.dart
index ae3ccbef7..026f4ca34 100644
--- a/lib/src/ast/sass/statement/warn_rule.dart
+++ b/lib/src/ast/sass/statement/warn_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -14,8 +13,7 @@ import '../statement.dart';
/// This prints a Sass value—usually a string—to warn the user of something.
///
/// {@category AST}
-@sealed
-class WarnRule implements Statement {
+final class WarnRule implements Statement {
/// The expression to print.
final Expression expression;
diff --git a/lib/src/ast/sass/statement/while_rule.dart b/lib/src/ast/sass/statement/while_rule.dart
index 18e8f6f94..34b39d52a 100644
--- a/lib/src/ast/sass/statement/while_rule.dart
+++ b/lib/src/ast/sass/statement/while_rule.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
@@ -16,8 +15,7 @@ import 'parent.dart';
/// `true`.
///
/// {@category AST}
-@sealed
-class WhileRule extends ParentStatement> {
+final class WhileRule extends ParentStatement> {
/// The condition that determines whether the block will be executed.
final Expression condition;
diff --git a/lib/src/ast/sass/supports_condition.dart b/lib/src/ast/sass/supports_condition.dart
index 53f96bb38..4b38d304e 100644
--- a/lib/src/ast/sass/supports_condition.dart
+++ b/lib/src/ast/sass/supports_condition.dart
@@ -2,12 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
-
import 'node.dart';
/// An abstract class for defining the condition a `@supports` rule selects.
///
/// {@category AST}
-@sealed
-abstract class SupportsCondition extends SassNode {}
+abstract interface class SupportsCondition implements SassNode {}
diff --git a/lib/src/ast/sass/supports_condition/anything.dart b/lib/src/ast/sass/supports_condition/anything.dart
index 4dbece4b2..91d90024a 100644
--- a/lib/src/ast/sass/supports_condition/anything.dart
+++ b/lib/src/ast/sass/supports_condition/anything.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../interpolation.dart';
@@ -12,8 +11,7 @@ import '../supports_condition.dart';
/// `` production.
///
/// {@category AST}
-@sealed
-class SupportsAnything implements SupportsCondition {
+final class SupportsAnything implements SupportsCondition {
/// The contents of the condition.
final Interpolation contents;
diff --git a/lib/src/ast/sass/supports_condition/declaration.dart b/lib/src/ast/sass/supports_condition/declaration.dart
index d29d717c9..322731018 100644
--- a/lib/src/ast/sass/supports_condition/declaration.dart
+++ b/lib/src/ast/sass/supports_condition/declaration.dart
@@ -13,8 +13,7 @@ import '../supports_condition.dart';
/// supported.
///
/// {@category AST}
-@sealed
-class SupportsDeclaration implements SupportsCondition {
+final class SupportsDeclaration implements SupportsCondition {
/// The name of the declaration being tested.
final Expression name;
@@ -33,12 +32,11 @@ class SupportsDeclaration implements SupportsCondition {
///
/// @nodoc
@internal
- bool get isCustomProperty {
- var name = this.name;
- return name is StringExpression &&
- !name.hasQuotes &&
- name.text.initialPlain.startsWith('--');
- }
+ bool get isCustomProperty => switch (name) {
+ StringExpression(hasQuotes: false, :var text) =>
+ text.initialPlain.startsWith('--'),
+ _ => false
+ };
SupportsDeclaration(this.name, this.value, this.span);
diff --git a/lib/src/ast/sass/supports_condition/function.dart b/lib/src/ast/sass/supports_condition/function.dart
index 73bdb9bda..dd9ac5b29 100644
--- a/lib/src/ast/sass/supports_condition/function.dart
+++ b/lib/src/ast/sass/supports_condition/function.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../interpolation.dart';
@@ -11,8 +10,7 @@ import '../supports_condition.dart';
/// A function-syntax condition.
///
/// {@category AST}
-@sealed
-class SupportsFunction implements SupportsCondition {
+final class SupportsFunction implements SupportsCondition {
/// The name of the function.
final Interpolation name;
diff --git a/lib/src/ast/sass/supports_condition/interpolation.dart b/lib/src/ast/sass/supports_condition/interpolation.dart
index 9fbd85829..839fccf9f 100644
--- a/lib/src/ast/sass/supports_condition/interpolation.dart
+++ b/lib/src/ast/sass/supports_condition/interpolation.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../expression.dart';
@@ -11,8 +10,7 @@ import '../supports_condition.dart';
/// An interpolated condition.
///
/// {@category AST}
-@sealed
-class SupportsInterpolation implements SupportsCondition {
+final class SupportsInterpolation implements SupportsCondition {
/// The expression in the interpolation.
final Expression expression;
diff --git a/lib/src/ast/sass/supports_condition/negation.dart b/lib/src/ast/sass/supports_condition/negation.dart
index 4187f3793..23cd7193e 100644
--- a/lib/src/ast/sass/supports_condition/negation.dart
+++ b/lib/src/ast/sass/supports_condition/negation.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../supports_condition.dart';
@@ -11,8 +10,7 @@ import 'operation.dart';
/// A negated condition.
///
/// {@category AST}
-@sealed
-class SupportsNegation implements SupportsCondition {
+final class SupportsNegation implements SupportsCondition {
/// The condition that's been negated.
final SupportsCondition condition;
diff --git a/lib/src/ast/sass/supports_condition/operation.dart b/lib/src/ast/sass/supports_condition/operation.dart
index 3e4fc5113..f072fc2e3 100644
--- a/lib/src/ast/sass/supports_condition/operation.dart
+++ b/lib/src/ast/sass/supports_condition/operation.dart
@@ -2,7 +2,6 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../supports_condition.dart';
@@ -11,8 +10,7 @@ import 'negation.dart';
/// An operation defining the relationship between two conditions.
///
/// {@category AST}
-@sealed
-class SupportsOperation implements SupportsCondition {
+final class SupportsOperation implements SupportsCondition {
/// The left-hand operand.
final SupportsCondition left;
diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart
index 8b694e430..953ccf7aa 100644
--- a/lib/src/ast/selector.dart
+++ b/lib/src/ast/selector.dart
@@ -3,12 +3,15 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
+import '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../visitor/any_selector.dart';
import '../visitor/interface/selector.dart';
import '../visitor/serialize.dart';
+import 'node.dart';
import 'selector/complex.dart';
import 'selector/list.dart';
import 'selector/placeholder.dart';
@@ -38,7 +41,7 @@ export 'selector/universal.dart';
/// Selectors have structural equality semantics.
///
/// {@category AST}
-abstract class Selector {
+abstract base class Selector implements AstNode {
/// Whether this selector, and complex selectors containing it, should not be
/// emitted.
///
@@ -76,19 +79,23 @@ abstract class Selector {
@internal
bool get isUseless => accept(const _IsUselessVisitor());
+ final FileSpan span;
+
+ Selector(this.span);
+
/// Prints a warning if [this] is a bogus selector.
///
/// This may only be called from within a custom Sass function. This will
- /// throw a [SassScriptException] in Dart Sass 2.0.0.
+ /// throw a [SassException] in Dart Sass 2.0.0.
void assertNotBogus({String? name}) {
if (!isBogus) return;
- warn(
+ warnForDeprecation(
(name == null ? '' : '\$$name: ') +
'$this is not valid CSS.\n'
'This will be an error in Dart Sass 2.0.0.\n'
'\n'
'More info: https://sass-lang.com/d/bogus-combinators',
- deprecation: true);
+ Deprecation.bogusCombinators);
}
/// Calls the appropriate visit method on [visitor].
@@ -114,16 +121,17 @@ class _IsInvisibleVisitor with AnySelectorVisitor {
bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true;
bool visitPseudoSelector(PseudoSelector pseudo) {
- var selector = pseudo.selector;
- if (selector == null) return false;
-
- // We don't consider `:not(%foo)` to be invisible because, semantically, it
- // means "doesn't match this selector that matches nothing", so it's
- // equivalent to *. If the entire compound selector is composed of `:not`s
- // with invisible lists, the serializer emits it as `*`.
- return pseudo.name == 'not'
- ? (includeBogus && selector.isBogus)
- : selector.accept(this);
+ if (pseudo.selector case var selector?) {
+ // We don't consider `:not(%foo)` to be invisible because, semantically,
+ // it means "doesn't match this selector that matches nothing", so it's
+ // equivalent to *. If the entire compound selector is composed of `:not`s
+ // with invisible lists, the serializer emits it as `*`.
+ return pseudo.name == 'not'
+ ? (includeBogus && selector.isBogus)
+ : selector.accept(this);
+ } else {
+ return false;
+ }
}
}
diff --git a/lib/src/ast/selector/attribute.dart b/lib/src/ast/selector/attribute.dart
index 0fbae6a29..dcdedf54a 100644
--- a/lib/src/ast/selector/attribute.dart
+++ b/lib/src/ast/selector/attribute.dart
@@ -2,7 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../visitor/interface/selector.dart';
import '../selector.dart';
@@ -13,8 +13,7 @@ import '../selector.dart';
/// value matching certain conditions as well.
///
/// {@category AST}
-@sealed
-class AttributeSelector extends SimpleSelector {
+final class AttributeSelector extends SimpleSelector {
/// The name of the attribute being selected for.
final QualifiedName name;
@@ -44,15 +43,17 @@ class AttributeSelector extends SimpleSelector {
/// Creates an attribute selector that matches any element with a property of
/// the given name.
- AttributeSelector(this.name)
+ AttributeSelector(this.name, FileSpan span)
: op = null,
value = null,
- modifier = null;
+ modifier = null,
+ super(span);
/// Creates an attribute selector that matches an element with a property
/// named [name], whose value matches [value] based on the semantics of [op].
- AttributeSelector.withOperator(this.name, this.op, this.value,
- {this.modifier});
+ AttributeSelector.withOperator(this.name, this.op, this.value, FileSpan span,
+ {this.modifier})
+ : super(span);
T accept(SelectorVisitor visitor) =>
visitor.visitAttributeSelector(this);
diff --git a/lib/src/ast/selector/class.dart b/lib/src/ast/selector/class.dart
index 513d46d4e..b01ce9da5 100644
--- a/lib/src/ast/selector/class.dart
+++ b/lib/src/ast/selector/class.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../visitor/interface/selector.dart';
import '../selector.dart';
@@ -13,12 +14,11 @@ import '../selector.dart';
/// the given name.
///
/// {@category AST}
-@sealed
-class ClassSelector extends SimpleSelector {
+final class ClassSelector extends SimpleSelector {
/// The class name this selects for.
final String name;
- ClassSelector(this.name);
+ ClassSelector(this.name, FileSpan span) : super(span);
bool operator ==(Object other) =>
other is ClassSelector && other.name == name;
@@ -27,7 +27,7 @@ class ClassSelector extends SimpleSelector {
/// @nodoc
@internal
- ClassSelector addSuffix(String suffix) => ClassSelector(name + suffix);
+ ClassSelector addSuffix(String suffix) => ClassSelector(name + suffix, span);
int get hashCode => name.hashCode;
}
diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart
index e5eb6cd25..3d97729ca 100644
--- a/lib/src/ast/selector/complex.dart
+++ b/lib/src/ast/selector/complex.dart
@@ -3,12 +3,14 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../extend/functions.dart';
import '../../logger.dart';
import '../../parse/selector.dart';
import '../../utils.dart';
import '../../visitor/interface/selector.dart';
+import '../css/value.dart';
import '../selector.dart';
/// A complex selector.
@@ -18,14 +20,13 @@ import '../selector.dart';
///
/// {@category AST}
/// {@category Parsing}
-@sealed
-class ComplexSelector extends Selector {
+final class ComplexSelector extends Selector {
/// This selector's leading combinators.
///
/// If this is empty, that indicates that it has no leading combinator. If
/// it's more than one element, that means it's invalid CSS; however, we still
/// support this for backwards-compatibility purposes.
- final List leadingCombinators;
+ final List> leadingCombinators;
/// The components of this selector.
///
@@ -60,17 +61,20 @@ class ComplexSelector extends Selector {
///
/// @nodoc
@internal
- CompoundSelector? get singleCompound => leadingCombinators.isEmpty &&
- components.length == 1 &&
- components.first.combinators.isEmpty
- ? components.first.selector
- : null;
-
- ComplexSelector(Iterable leadingCombinators,
- Iterable components,
+ CompoundSelector? get singleCompound {
+ if (leadingCombinators.isNotEmpty) return null;
+ return switch (components) {
+ [ComplexSelectorComponent(:var selector, combinators: [])] => selector,
+ _ => null
+ };
+ }
+
+ ComplexSelector(Iterable> leadingCombinators,
+ Iterable components, FileSpan span,
{this.lineBreak = false})
: leadingCombinators = List.unmodifiable(leadingCombinators),
- components = List.unmodifiable(components) {
+ components = List.unmodifiable(components),
+ super(span) {
if (this.leadingCombinators.isEmpty && this.components.isEmpty) {
throw ArgumentError(
"leadingCombinators and components may not both be empty.");
@@ -109,22 +113,18 @@ class ComplexSelector extends Selector {
///
/// @nodoc
@internal
- ComplexSelector withAdditionalCombinators(List combinators,
+ ComplexSelector withAdditionalCombinators(
+ List> combinators,
{bool forceLineBreak = false}) {
- if (combinators.isEmpty) {
- return this;
- } else if (components.isEmpty) {
- return ComplexSelector([...leadingCombinators, ...combinators], const [],
- lineBreak: lineBreak || forceLineBreak);
- } else {
- return ComplexSelector(
- leadingCombinators,
- [
- ...components.exceptLast,
- components.last.withAdditionalCombinators(combinators)
- ],
- lineBreak: lineBreak || forceLineBreak);
- }
+ if (combinators.isEmpty) return this;
+ return switch (components) {
+ [...var initial, var last] => ComplexSelector(leadingCombinators,
+ [...initial, last.withAdditionalCombinators(combinators)], span,
+ lineBreak: lineBreak || forceLineBreak),
+ [] => ComplexSelector(
+ [...leadingCombinators, ...combinators], const [], span,
+ lineBreak: lineBreak || forceLineBreak)
+ };
}
/// Returns a copy of `this` with an additional [component] added to the end.
@@ -132,11 +132,14 @@ class ComplexSelector extends Selector {
/// If [forceLineBreak] is `true`, this will mark the new complex selector as
/// having a line break.
///
+ /// The [span] is used for the new selector.
+ ///
/// @nodoc
@internal
- ComplexSelector withAdditionalComponent(ComplexSelectorComponent component,
+ ComplexSelector withAdditionalComponent(
+ ComplexSelectorComponent component, FileSpan span,
{bool forceLineBreak = false}) =>
- ComplexSelector(leadingCombinators, [...components, component],
+ ComplexSelector(leadingCombinators, [...components, component], span,
lineBreak: lineBreak || forceLineBreak);
/// Returns a copy of `this` with [child]'s combinators added to the end.
@@ -144,30 +147,34 @@ class ComplexSelector extends Selector {
/// If [child] has [leadingCombinators], they're appended to `this`'s last
/// combinator. This does _not_ resolve parent selectors.
///
+ /// The [span] is used for the new selector.
+ ///
/// If [forceLineBreak] is `true`, this will mark the new complex selector as
/// having a line break.
///
/// @nodoc
@internal
- ComplexSelector concatenate(ComplexSelector child,
+ ComplexSelector concatenate(ComplexSelector child, FileSpan span,
{bool forceLineBreak = false}) {
if (child.leadingCombinators.isEmpty) {
return ComplexSelector(
- leadingCombinators, [...components, ...child.components],
- lineBreak: lineBreak || child.lineBreak || forceLineBreak);
- } else if (components.isEmpty) {
- return ComplexSelector(
- [...leadingCombinators, ...child.leadingCombinators],
- child.components,
+ leadingCombinators, [...components, ...child.components], span,
lineBreak: lineBreak || child.lineBreak || forceLineBreak);
- } else {
+ } else if (components case [...var initial, var last]) {
return ComplexSelector(
leadingCombinators,
[
- ...components.exceptLast,
- components.last.withAdditionalCombinators(child.leadingCombinators),
+ ...initial,
+ last.withAdditionalCombinators(child.leadingCombinators),
...child.components
],
+ span,
+ lineBreak: lineBreak || child.lineBreak || forceLineBreak);
+ } else {
+ return ComplexSelector(
+ [...leadingCombinators, ...child.leadingCombinators],
+ child.components,
+ span,
lineBreak: lineBreak || child.lineBreak || forceLineBreak);
}
}
diff --git a/lib/src/ast/selector/complex_component.dart b/lib/src/ast/selector/complex_component.dart
index 61bdd9330..70f6d8e42 100644
--- a/lib/src/ast/selector/complex_component.dart
+++ b/lib/src/ast/selector/complex_component.dart
@@ -3,8 +3,10 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../utils.dart';
+import '../css/value.dart';
import '../selector.dart';
/// A component of a [ComplexSelector].
@@ -12,8 +14,7 @@ import '../selector.dart';
/// This a [CompoundSelector] with one or more trailing [Combinator]s.
///
/// {@category AST}
-@sealed
-class ComplexSelectorComponent {
+final class ComplexSelectorComponent {
/// This component's compound selector.
final CompoundSelector selector;
@@ -22,9 +23,12 @@ class ComplexSelectorComponent {
/// If this is empty, that indicates that it has an implicit descendent
/// combinator. If it's more than one element, that means it's invalid CSS;
/// however, we still support this for backwards-compatibility purposes.
- final List combinators;
+ final List> combinators;
- ComplexSelectorComponent(this.selector, Iterable combinators)
+ final FileSpan span;
+
+ ComplexSelectorComponent(
+ this.selector, Iterable> combinators, this.span)
: combinators = List.unmodifiable(combinators);
/// Returns a copy of `this` with [combinators] added to the end of
@@ -33,11 +37,11 @@ class ComplexSelectorComponent {
/// @nodoc
@internal
ComplexSelectorComponent withAdditionalCombinators(
- List combinators) =>
+ List> combinators) =>
combinators.isEmpty
? this
: ComplexSelectorComponent(
- selector, [...this.combinators, ...combinators]);
+ selector, [...this.combinators, ...combinators], span);
int get hashCode => selector.hashCode ^ listHash(combinators);
diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart
index 1c3905154..c36662cb0 100644
--- a/lib/src/ast/selector/compound.dart
+++ b/lib/src/ast/selector/compound.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../extend/functions.dart';
import '../../logger.dart';
@@ -18,8 +19,7 @@ import '../selector.dart';
///
/// {@category AST}
/// {@category Parsing}
-@sealed
-class CompoundSelector extends Selector {
+final class CompoundSelector extends Selector {
/// The components of this selector.
///
/// This is never empty.
@@ -43,8 +43,9 @@ class CompoundSelector extends Selector {
SimpleSelector? get singleSimple =>
components.length == 1 ? components.first : null;
- CompoundSelector(Iterable components)
- : components = List.unmodifiable(components) {
+ CompoundSelector(Iterable components, FileSpan span)
+ : components = List.unmodifiable(components),
+ super(span) {
if (this.components.isEmpty) {
throw ArgumentError("components may not be empty.");
}
diff --git a/lib/src/ast/selector/id.dart b/lib/src/ast/selector/id.dart
index 010bd2161..dc820fba3 100644
--- a/lib/src/ast/selector/id.dart
+++ b/lib/src/ast/selector/id.dart
@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../visitor/interface/selector.dart';
import '../selector.dart';
@@ -14,20 +15,19 @@ import '../selector.dart';
/// This selects elements whose `id` attribute exactly matches the given name.
///
/// {@category AST}
-@sealed
-class IDSelector extends SimpleSelector {
+final class IDSelector extends SimpleSelector {
/// The ID name this selects for.
final String name;
int get specificity => math.pow(super.specificity, 2) as int;
- IDSelector(this.name);
+ IDSelector(this.name, FileSpan span) : super(span);
T accept(SelectorVisitor visitor) => visitor.visitIDSelector(this);
/// @nodoc
@internal
- IDSelector addSuffix(String suffix) => IDSelector(name + suffix);
+ IDSelector addSuffix(String suffix) => IDSelector(name + suffix, span);
/// @nodoc
@internal
diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart
index f87a52daa..d432bbfaa 100644
--- a/lib/src/ast/selector/list.dart
+++ b/lib/src/ast/selector/list.dart
@@ -3,14 +3,20 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
+import '../../exception.dart';
import '../../extend/functions.dart';
+import '../../interpolation_map.dart';
import '../../logger.dart';
import '../../parse/selector.dart';
import '../../utils.dart';
-import '../../exception.dart';
+import '../../util/iterable.dart';
+import '../../util/span.dart';
import '../../value.dart';
import '../../visitor/interface/selector.dart';
+import '../../visitor/selector_search.dart';
+import '../css/value.dart';
import '../selector.dart';
/// A selector list.
@@ -20,17 +26,12 @@ import '../selector.dart';
///
/// {@category AST}
/// {@category Parsing}
-@sealed
-class SelectorList extends Selector {
+final class SelectorList extends Selector {
/// The components of this selector.
///
/// This is never empty.
final List components;
- /// Whether this contains a [ParentSelector].
- bool get _containsParentSelector =>
- components.any(_complexContainsParentSelector);
-
/// Returns a SassScript list that represents this selector.
///
/// This has the same format as a list returned by `selector-parse()`.
@@ -48,8 +49,9 @@ class SelectorList extends Selector {
}), ListSeparator.comma);
}
- SelectorList(Iterable components)
- : components = List.unmodifiable(components) {
+ SelectorList(Iterable components, FileSpan span)
+ : components = List.unmodifiable(components),
+ super(span) {
if (this.components.isEmpty) {
throw ArgumentError("components may not be empty.");
}
@@ -61,15 +63,20 @@ class SelectorList extends Selector {
/// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or
/// [PlaceholderSelector]s are allowed in this selector, respectively.
///
+ /// If passed, [interpolationMap] maps the text of [contents] back to the
+ /// original location of the selector in the source file.
+ ///
/// Throws a [SassFormatException] if parsing fails.
factory SelectorList.parse(String contents,
{Object? url,
Logger? logger,
+ InterpolationMap? interpolationMap,
bool allowParent = true,
bool allowPlaceholder = true}) =>
SelectorParser(contents,
url: url,
logger: logger,
+ interpolationMap: interpolationMap,
allowParent: allowParent,
allowPlaceholder: allowPlaceholder)
.parse();
@@ -84,10 +91,10 @@ class SelectorList extends Selector {
var contents = [
for (var complex1 in components)
for (var complex2 in other.components)
- ...?unifyComplex([complex1, complex2])
+ ...?unifyComplex([complex1, complex2], complex1.span)
];
- return contents.isEmpty ? null : SelectorList(contents);
+ return contents.isEmpty ? null : SelectorList(contents, span);
}
/// Returns a new list with all [ParentSelector]s replaced with [parent].
@@ -101,16 +108,18 @@ class SelectorList extends Selector {
SelectorList resolveParentSelectors(SelectorList? parent,
{bool implicitParent = true}) {
if (parent == null) {
- if (!_containsParentSelector) return this;
- throw SassScriptException(
- 'Top-level selectors may not contain the parent selector "&".');
+ var parentSelector = accept(const _ParentSelectorVisitor());
+ if (parentSelector == null) return this;
+ throw SassException(
+ 'Top-level selectors may not contain the parent selector "&".',
+ parentSelector.span);
}
return SelectorList(flattenVertically(components.map((complex) {
- if (!_complexContainsParentSelector(complex)) {
+ if (!_containsParentSelector(complex)) {
if (!implicitParent) return [complex];
- return parent.components
- .map((parentComplex) => parentComplex.concatenate(complex));
+ return parent.components.map((parentComplex) =>
+ parentComplex.concatenate(complex, complex.span));
}
var newComplexes = [];
@@ -119,40 +128,41 @@ class SelectorList extends Selector {
if (resolved == null) {
if (newComplexes.isEmpty) {
newComplexes.add(ComplexSelector(
- complex.leadingCombinators, [component],
+ complex.leadingCombinators, [component], complex.span,
lineBreak: false));
} else {
for (var i = 0; i < newComplexes.length; i++) {
- newComplexes[i] =
- newComplexes[i].withAdditionalComponent(component);
+ newComplexes[i] = newComplexes[i]
+ .withAdditionalComponent(component, complex.span);
}
}
} else if (newComplexes.isEmpty) {
- newComplexes.addAll(resolved);
+ newComplexes.addAll(complex.leadingCombinators.isEmpty
+ ? resolved
+ : resolved.map((resolvedComplex) => ComplexSelector(
+ resolvedComplex.leadingCombinators.isEmpty
+ ? complex.leadingCombinators
+ : [
+ ...complex.leadingCombinators,
+ ...resolvedComplex.leadingCombinators
+ ],
+ resolvedComplex.components,
+ complex.span,
+ lineBreak: resolvedComplex.lineBreak)));
} else {
var previousComplexes = newComplexes;
newComplexes = [
for (var newComplex in previousComplexes)
for (var resolvedComplex in resolved)
- newComplex.concatenate(resolvedComplex)
+ newComplex.concatenate(resolvedComplex, newComplex.span)
];
}
}
return newComplexes;
- })));
+ })), span);
}
- /// Returns whether [complex] contains a [ParentSelector].
- bool _complexContainsParentSelector(ComplexSelector complex) =>
- complex.components
- .any((component) => component.selector.components.any((simple) {
- if (simple is ParentSelector) return true;
- if (simple is! PseudoSelector) return false;
- var selector = simple.selector;
- return selector != null && selector._containsParentSelector;
- }));
-
/// Returns a new selector list based on [component] with all
/// [ParentSelector]s replaced with [parent].
///
@@ -163,59 +173,84 @@ class SelectorList extends Selector {
var containsSelectorPseudo = simples.any((simple) {
if (simple is! PseudoSelector) return false;
var selector = simple.selector;
- return selector != null && selector._containsParentSelector;
+ return selector != null && _containsParentSelector(selector);
});
if (!containsSelectorPseudo && simples.first is! ParentSelector) {
return null;
}
var resolvedSimples = containsSelectorPseudo
- ? simples.map((simple) {
- if (simple is! PseudoSelector) return simple;
- var selector = simple.selector;
- if (selector == null) return simple;
- if (!selector._containsParentSelector) return simple;
- return simple.withSelector(
- selector.resolveParentSelectors(parent, implicitParent: false));
- })
+ ? simples.map((simple) => switch (simple) {
+ PseudoSelector(:var selector?)
+ when _containsParentSelector(selector) =>
+ simple.withSelector(selector.resolveParentSelectors(parent,
+ implicitParent: false)),
+ _ => simple
+ })
: simples;
var parentSelector = simples.first;
- if (parentSelector is! ParentSelector) {
- return [
- ComplexSelector(const [], [
- ComplexSelectorComponent(
- CompoundSelector(resolvedSimples), component.combinators)
- ])
- ];
- } else if (simples.length == 1 && parentSelector.suffix == null) {
- return parent.withAdditionalCombinators(component.combinators).components;
+ try {
+ if (parentSelector is! ParentSelector) {
+ return [
+ ComplexSelector(const [], [
+ ComplexSelectorComponent(
+ CompoundSelector(resolvedSimples, component.selector.span),
+ component.combinators,
+ component.span)
+ ], component.span)
+ ];
+ } else if (simples.length == 1 && parentSelector.suffix == null) {
+ return parent
+ .withAdditionalCombinators(component.combinators)
+ .components;
+ }
+ } on SassException catch (error, stackTrace) {
+ throwWithTrace(
+ error.withAdditionalSpan(parentSelector.span, "parent selector"),
+ error,
+ stackTrace);
}
return parent.components.map((complex) {
- var lastComponent = complex.components.last;
- if (lastComponent.combinators.isNotEmpty) {
- throw SassScriptException(
- 'Parent "$complex" is incompatible with this selector.');
- }
+ try {
+ var lastComponent = complex.components.last;
+ if (lastComponent.combinators.isNotEmpty) {
+ throw MultiSpanSassException(
+ 'Selector "$complex" can\'t be used as a parent in a compound '
+ 'selector.',
+ lastComponent.span.trimRight(),
+ "outer selector",
+ {parentSelector.span: "parent selector"});
+ }
+
+ var suffix = parentSelector.suffix;
+ var lastSimples = lastComponent.selector.components;
+ var last = CompoundSelector(
+ suffix == null
+ ? [...lastSimples, ...resolvedSimples.skip(1)]
+ : [
+ ...lastSimples.exceptLast,
+ lastSimples.last.addSuffix(suffix),
+ ...resolvedSimples.skip(1)
+ ],
+ component.selector.span);
- var suffix = parentSelector.suffix;
- var lastSimples = lastComponent.selector.components;
- var last = CompoundSelector(suffix == null
- ? [...lastSimples, ...resolvedSimples.skip(1)]
- : [
- ...lastSimples.exceptLast,
- lastSimples.last.addSuffix(suffix),
- ...resolvedSimples.skip(1)
- ]);
-
- return ComplexSelector(
- complex.leadingCombinators,
- [
- ...complex.components.exceptLast,
- ComplexSelectorComponent(last, component.combinators)
- ],
- lineBreak: complex.lineBreak);
+ return ComplexSelector(
+ complex.leadingCombinators,
+ [
+ ...complex.components.exceptLast,
+ ComplexSelectorComponent(
+ last, component.combinators, component.span)
+ ],
+ component.span,
+ lineBreak: complex.lineBreak);
+ } on SassException catch (error, stackTrace) {
+ throwWithTrace(
+ error.withAdditionalSpan(parentSelector.span, "parent selector"),
+ error,
+ stackTrace);
+ }
});
}
@@ -229,14 +264,28 @@ class SelectorList extends Selector {
/// Returns a copy of `this` with [combinators] added to the end of each
/// complex selector in [components].
@internal
- SelectorList withAdditionalCombinators(List combinators) =>
+ SelectorList withAdditionalCombinators(
+ List> combinators) =>
combinators.isEmpty
? this
- : SelectorList(components.map(
- (complex) => complex.withAdditionalCombinators(combinators)));
+ : SelectorList(
+ components.map(
+ (complex) => complex.withAdditionalCombinators(combinators)),
+ span);
int get hashCode => listHash(components);
bool operator ==(Object other) =>
other is SelectorList && listEquals(components, other.components);
}
+
+/// Returns whether [selector] recursively contains a parent selector.
+bool _containsParentSelector(Selector selector) =>
+ selector.accept(const _ParentSelectorVisitor()) != null;
+
+/// A visitor for finding the first [ParentSelector] in a given selector.
+class _ParentSelectorVisitor with SelectorSearchVisitor {
+ const _ParentSelectorVisitor();
+
+ ParentSelector visitParentSelector(ParentSelector selector) => selector;
+}
diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart
index 461d5e480..18e898652 100644
--- a/lib/src/ast/selector/parent.dart
+++ b/lib/src/ast/selector/parent.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../visitor/interface/selector.dart';
import '../selector.dart';
@@ -13,8 +14,7 @@ import '../selector.dart';
/// document.
///
/// {@category AST}
-@sealed
-class ParentSelector extends SimpleSelector {
+final class ParentSelector extends SimpleSelector {
/// The suffix that will be added to the parent selector after it's been
/// resolved.
///
@@ -22,7 +22,7 @@ class ParentSelector extends SimpleSelector {
/// indicating that the parent selector will not be modified.
final String? suffix;
- ParentSelector({this.suffix});
+ ParentSelector(FileSpan span, {this.suffix}) : super(span);
T accept(SelectorVisitor visitor) => visitor.visitParentSelector(this);
diff --git a/lib/src/ast/selector/placeholder.dart b/lib/src/ast/selector/placeholder.dart
index a7b935322..a99005d21 100644
--- a/lib/src/ast/selector/placeholder.dart
+++ b/lib/src/ast/selector/placeholder.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../util/character.dart' as character;
import '../../visitor/interface/selector.dart';
@@ -15,8 +16,7 @@ import '../selector.dart';
/// emitting a CSS document.
///
/// {@category AST}
-@sealed
-class PlaceholderSelector extends SimpleSelector {
+final class PlaceholderSelector extends SimpleSelector {
/// The name of the placeholder.
final String name;
@@ -24,7 +24,7 @@ class PlaceholderSelector extends SimpleSelector {
/// with `-` or `_`).
bool get isPrivate => character.isPrivate(name);
- PlaceholderSelector(this.name);
+ PlaceholderSelector(this.name, FileSpan span) : super(span);
T accept(SelectorVisitor visitor) =>
visitor.visitPlaceholderSelector(this);
@@ -32,7 +32,7 @@ class PlaceholderSelector extends SimpleSelector {
/// @nodoc
@internal
PlaceholderSelector addSuffix(String suffix) =>
- PlaceholderSelector(name + suffix);
+ PlaceholderSelector(name + suffix, span);
bool operator ==(Object other) =>
other is PlaceholderSelector && other.name == name;
diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart
index 7840eccab..44a263d15 100644
--- a/lib/src/ast/selector/pseudo.dart
+++ b/lib/src/ast/selector/pseudo.dart
@@ -5,6 +5,7 @@
import 'package:charcode/charcode.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../utils.dart';
import '../../util/nullable.dart';
@@ -19,8 +20,7 @@ import '../selector.dart';
/// ensure that extension and other selector operations work properly.
///
/// {@category AST}
-@sealed
-class PseudoSelector extends SimpleSelector {
+final class PseudoSelector extends SimpleSelector {
/// The name of this selector.
final String name;
@@ -104,11 +104,12 @@ class PseudoSelector extends SimpleSelector {
}
}();
- PseudoSelector(this.name,
+ PseudoSelector(this.name, FileSpan span,
{bool element = false, this.argument, this.selector})
: isClass = !element && !_isFakePseudoElement(name),
isSyntacticClass = !element,
- normalizedName = unvendor(name);
+ normalizedName = unvendor(name),
+ super(span);
/// Returns whether [name] is the name of a pseudo-element that can be written
/// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or
@@ -135,14 +136,15 @@ class PseudoSelector extends SimpleSelector {
/// Returns a new [PseudoSelector] based on this, but with the selector
/// replaced with [selector].
- PseudoSelector withSelector(SelectorList selector) => PseudoSelector(name,
- element: isElement, argument: argument, selector: selector);
+ PseudoSelector withSelector(SelectorList selector) =>
+ PseudoSelector(name, span,
+ element: isElement, argument: argument, selector: selector);
/// @nodoc
@internal
PseudoSelector addSuffix(String suffix) {
if (argument != null || selector != null) super.addSuffix(suffix);
- return PseudoSelector(name + suffix, element: isElement);
+ return PseudoSelector(name + suffix, span, element: isElement);
}
/// @nodoc
@@ -154,12 +156,11 @@ class PseudoSelector extends SimpleSelector {
(simple.isHost || simple.selector != null))) {
return null;
}
- } else if (compound.length == 1) {
- var other = compound.first;
- if (other is UniversalSelector ||
- (other is PseudoSelector && (other.isHost || other.isHostContext))) {
- return other.unify([this]);
- }
+ } else if (compound case [var other]
+ when other is UniversalSelector ||
+ (other is PseudoSelector &&
+ (other.isHost || other.isHostContext))) {
+ return other.unify([this]);
}
if (compound.contains(this)) return compound;
@@ -167,7 +168,7 @@ class PseudoSelector extends SimpleSelector {
var result = [];
var addedThis = false;
for (var simple in compound) {
- if (simple is PseudoSelector && simple.isElement) {
+ if (simple case PseudoSelector(isElement: true)) {
// A given compound selector may only contain one pseudo element. If
// [compound] has a different one than [this], unification fails.
if (isElement) return null;
@@ -200,7 +201,8 @@ class PseudoSelector extends SimpleSelector {
// Fall back to the logic defined in functions.dart, which knows how to
// compare selector pseudoclasses against raw selectors.
- return CompoundSelector([this]).isSuperselector(CompoundSelector([other]));
+ return CompoundSelector([this], span)
+ .isSuperselector(CompoundSelector([other], span));
}
T accept(SelectorVisitor visitor) => visitor.visitPseudoSelector(this);
diff --git a/lib/src/ast/selector/qualified_name.dart b/lib/src/ast/selector/qualified_name.dart
index 05bb4a084..6a594a2c7 100644
--- a/lib/src/ast/selector/qualified_name.dart
+++ b/lib/src/ast/selector/qualified_name.dart
@@ -2,15 +2,12 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:meta/meta.dart';
-
/// A [qualified name].
///
/// [qualified name]: https://www.w3.org/TR/css3-namespace/#css-qnames
///
/// {@category AST}
-@sealed
-class QualifiedName {
+final class QualifiedName {
/// The identifier name.
final String name;
diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart
index 599b43c8e..0526eed72 100644
--- a/lib/src/ast/selector/simple.dart
+++ b/lib/src/ast/selector/simple.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../exception.dart';
import '../../logger.dart';
@@ -26,7 +27,7 @@ final _subselectorPseudos = {
///
/// {@category AST}
/// {@category Parsing}
-abstract class SimpleSelector extends Selector {
+abstract base class SimpleSelector extends Selector {
/// This selector's specificity.
///
/// Specificity is represented in base 1000. The spec says this should be
@@ -34,7 +35,7 @@ abstract class SimpleSelector extends Selector {
/// sequence will contain 1000 simple selectors.
int get specificity => 1000;
- SimpleSelector();
+ SimpleSelector(FileSpan span) : super(span);
/// Parses a simple selector from [contents].
///
@@ -57,8 +58,8 @@ abstract class SimpleSelector extends Selector {
///
/// @nodoc
@internal
- SimpleSelector addSuffix(String suffix) =>
- throw SassScriptException('Invalid parent selector "$this"');
+ SimpleSelector addSuffix(String suffix) => throw MultiSpanSassException(
+ 'Selector "$this" can\'t have a suffix', span, "outer selector", {});
/// Returns the components of a [CompoundSelector] that matches only elements
/// matched by both this and [compound].
@@ -73,12 +74,11 @@ abstract class SimpleSelector extends Selector {
/// @nodoc
@internal
List? unify(List compound) {
- if (compound.length == 1) {
- var other = compound.first;
- if (other is UniversalSelector ||
- (other is PseudoSelector && (other.isHost || other.isHostContext))) {
- return other.unify([this]);
- }
+ if (compound case [var other]
+ when other is UniversalSelector ||
+ (other is PseudoSelector &&
+ (other.isHost || other.isHostContext))) {
+ return other.unify([this]);
}
if (compound.contains(this)) return compound;
diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart
index 0430de768..d65f94a0c 100644
--- a/lib/src/ast/selector/type.dart
+++ b/lib/src/ast/selector/type.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../extend/functions.dart';
import '../../visitor/interface/selector.dart';
@@ -13,26 +14,25 @@ import '../selector.dart';
/// This selects elements whose name equals the given name.
///
/// {@category AST}
-@sealed
-class TypeSelector extends SimpleSelector {
+final class TypeSelector extends SimpleSelector {
/// The element name being selected.
final QualifiedName name;
int get specificity => 1;
- TypeSelector(this.name);
+ TypeSelector(this.name, FileSpan span) : super(span);
T accept(SelectorVisitor visitor) => visitor.visitTypeSelector(this);
/// @nodoc
@internal
TypeSelector addSuffix(String suffix) => TypeSelector(
- QualifiedName(name.name + suffix, namespace: name.namespace));
+ QualifiedName(name.name + suffix, namespace: name.namespace), span);
/// @nodoc
@internal
List? unify(List compound) {
- if (compound.first is UniversalSelector || compound.first is TypeSelector) {
+ if (compound.first case UniversalSelector() || TypeSelector()) {
var unified = unifyUniversalAndElement(this, compound.first);
if (unified == null) return null;
return [unified, ...compound.skip(1)];
diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart
index 2937a78f6..d714dcb6a 100644
--- a/lib/src/ast/selector/universal.dart
+++ b/lib/src/ast/selector/universal.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
+import 'package:source_span/source_span.dart';
import '../../extend/functions.dart';
import '../../visitor/interface/selector.dart';
@@ -11,8 +12,7 @@ import '../selector.dart';
/// Matches any element in the given namespace.
///
/// {@category AST}
-@sealed
-class UniversalSelector extends SimpleSelector {
+final class UniversalSelector extends SimpleSelector {
/// The selector namespace.
///
/// If this is `null`, this matches all elements in the default namespace. If
@@ -23,7 +23,7 @@ class UniversalSelector extends SimpleSelector {
int get specificity => 0;
- UniversalSelector({this.namespace});
+ UniversalSelector(FileSpan span, {this.namespace}) : super(span);
T accept(SelectorVisitor visitor) =>
visitor.visitUniversalSelector(this);
@@ -31,20 +31,23 @@ class UniversalSelector extends SimpleSelector {
/// @nodoc
@internal
List? unify(List compound) {
- var first = compound.first;
- if (first is UniversalSelector || first is TypeSelector) {
- var unified = unifyUniversalAndElement(this, first);
- if (unified == null) return null;
- return [unified, ...compound.skip(1)];
- } else if (compound.length == 1 &&
- first is PseudoSelector &&
- (first.isHost || first.isHostContext)) {
- return null;
- }
+ switch (compound) {
+ case [UniversalSelector() || TypeSelector(), ...var rest]:
+ var unified = unifyUniversalAndElement(this, compound.first);
+ if (unified == null) return null;
+ return [unified, ...rest];
+
+ case [PseudoSelector first] when first.isHost || first.isHostContext:
+ return null;
- if (namespace != null && namespace != "*") return [this, ...compound];
- if (compound.isNotEmpty) return compound;
- return [this];
+ case []:
+ return [this];
+
+ case _:
+ return namespace == null || namespace == "*"
+ ? compound
+ : [this, ...compound];
+ }
}
bool isSuperselector(SimpleSelector other) {
diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart
index d01dfcc40..0d95a5dd7 100644
--- a/lib/src/async_compile.dart
+++ b/lib/src/async_compile.dart
@@ -10,11 +10,13 @@ import 'ast/sass.dart';
import 'async_import_cache.dart';
import 'callable.dart';
import 'compile_result.dart';
+import 'deprecation.dart';
import 'importer.dart';
import 'importer/legacy_node.dart';
+import 'importer/no_op.dart';
import 'io.dart';
import 'logger.dart';
-import 'logger/terse.dart';
+import 'logger/deprecation_handling.dart';
import 'syntax.dart';
import 'utils.dart';
import 'visitor/async_evaluate.dart';
@@ -37,9 +39,14 @@ Future compileAsync(String path,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) async {
- TerseLogger? terseLogger;
- if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr());
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) async {
+ DeprecationHandlingLogger deprecationLogger = logger =
+ DeprecationHandlingLogger(logger ?? Logger.stderr(),
+ fatalDeprecations: {...?fatalDeprecations},
+ futureDeprecations: {...?futureDeprecations},
+ limitRepetition: !verbose);
// If the syntax is different than the importer would default to, we have to
// parse the file manually and we can't store it in the cache.
@@ -71,7 +78,7 @@ Future compileAsync(String path,
sourceMap,
charset);
- terseLogger?.summarize(node: nodeImporter != null);
+ deprecationLogger.summarize(js: nodeImporter != null);
return result;
}
@@ -96,9 +103,14 @@ Future compileStringAsync(String source,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) async {
- TerseLogger? terseLogger;
- if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr());
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) async {
+ DeprecationHandlingLogger deprecationLogger = logger =
+ DeprecationHandlingLogger(logger ?? Logger.stderr(),
+ fatalDeprecations: {...?fatalDeprecations},
+ futureDeprecations: {...?futureDeprecations},
+ limitRepetition: !verbose);
var stylesheet =
Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger);
@@ -108,7 +120,7 @@ Future compileStringAsync(String source,
logger,
importCache,
nodeImporter,
- importer ?? FilesystemImporter('.'),
+ importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')),
functions,
style,
useSpaces,
@@ -118,7 +130,7 @@ Future compileStringAsync(String source,
sourceMap,
charset);
- terseLogger?.summarize(node: nodeImporter != null);
+ deprecationLogger.summarize(js: nodeImporter != null);
return result;
}
@@ -158,9 +170,7 @@ Future _compileStylesheet(
var resultSourceMap = serializeResult.sourceMap;
if (resultSourceMap != null && importCache != null) {
- // TODO(nweiz): Don't explicitly use a type parameter when dart-lang/sdk#25490
- // is fixed.
- mapInPlace(
+ mapInPlace(
resultSourceMap.urls,
(url) => url == ''
? Uri.dataFromString(stylesheet.span.file.getText(0),
diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart
index e6865b0ad..96cbecc18 100644
--- a/lib/src/async_environment.dart
+++ b/lib/src/async_environment.dart
@@ -17,6 +17,7 @@ import 'extend/extension_store.dart';
import 'module.dart';
import 'module/forwarded_view.dart';
import 'module/shadowed_view.dart';
+import 'util/map.dart';
import 'util/merged_map_view.dart';
import 'util/nullable.dart';
import 'util/public_member_map_view.dart';
@@ -33,7 +34,7 @@ import 'visitor/clone_css.dart';
///
/// This tracks lexically-scoped information, such as variables, functions, and
/// mixins.
-class AsyncEnvironment {
+final class AsyncEnvironment {
/// The modules used in the current scope, indexed by their namespaces.
Map get modules => UnmodifiableMapView(_modules);
final Map _modules;
@@ -235,12 +236,11 @@ class AsyncEnvironment {
_globalModules[module] = nodeWithSpan;
_allModules.add(module);
- for (var name in _variables.first.keys) {
- if (module.variables.containsKey(name)) {
- throw SassScriptException(
- 'This module and the new module both define a variable named '
- '"\$$name".');
- }
+ if (_variables.first.keys.firstWhereOrNull(module.variables.containsKey)
+ case var name?) {
+ throw SassScriptException(
+ 'This module and the new module both define a variable named '
+ '"\$$name".');
}
} else {
if (_modules.containsKey(namespace)) {
@@ -299,11 +299,12 @@ class AsyncEnvironment {
larger = newMembers;
}
- for (var name in smaller.keys) {
- if (!larger.containsKey(name)) continue;
+ for (var (name, small) in smaller.pairs) {
+ var large = larger[name];
+ if (large == null) continue;
if (type == "variable"
? newModule.variableIdentity(name) == oldModule.variableIdentity(name)
- : larger[name] == smaller[name]) {
+ : large == small) {
continue;
}
@@ -321,82 +322,82 @@ class AsyncEnvironment {
///
/// This is called when [module] is `@import`ed.
void importForwards(Module module) {
- if (module is _EnvironmentModule) {
- var forwarded = module._environment._forwardedModules;
- if (forwarded == null) return;
-
- // Omit modules from [forwarded] that are already globally available and
- // forwarded in this module.
- var forwardedModules = _forwardedModules;
- if (forwardedModules != null) {
- forwarded = {
- for (var entry in forwarded.entries)
- if (!forwardedModules.containsKey(entry.key) ||
- !_globalModules.containsKey(entry.key))
- entry.key: entry.value,
- };
- } else {
- forwardedModules = _forwardedModules ??= {};
- }
+ if (module is! _EnvironmentModule) return;
+ var forwarded = module._environment._forwardedModules;
+ if (forwarded == null) return;
+
+ // Omit modules from [forwarded] that are already globally available and
+ // forwarded in this module.
+ var forwardedModules = _forwardedModules;
+ if (forwardedModules != null) {
+ forwarded = {
+ for (var (module, node) in forwarded.pairs)
+ if (!forwardedModules.containsKey(module) ||
+ !_globalModules.containsKey(module))
+ module: node,
+ };
+ } else {
+ forwardedModules = _forwardedModules ??= {};
+ }
- var forwardedVariableNames =
- forwarded.keys.expand((module) => module.variables.keys).toSet();
- var forwardedFunctionNames =
- forwarded.keys.expand((module) => module.functions.keys).toSet();
- var forwardedMixinNames =
- forwarded.keys.expand((module) => module.mixins.keys).toSet();
-
- if (atRoot) {
- // Hide members from modules that have already been imported or
- // forwarded that would otherwise conflict with the @imported members.
- for (var entry in _importedModules.entries.toList()) {
- var module = entry.key;
- var shadowed = ShadowedModuleView.ifNecessary(module,
- variables: forwardedVariableNames,
- mixins: forwardedMixinNames,
- functions: forwardedFunctionNames);
- if (shadowed != null) {
- _importedModules.remove(module);
- if (!shadowed.isEmpty) _importedModules[shadowed] = entry.value;
- }
+ var forwardedVariableNames = {
+ for (var module in forwarded.keys) ...module.variables.keys
+ };
+ var forwardedFunctionNames = {
+ for (var module in forwarded.keys) ...module.functions.keys
+ };
+ var forwardedMixinNames = {
+ for (var module in forwarded.keys) ...module.mixins.keys
+ };
+
+ if (atRoot) {
+ // Hide members from modules that have already been imported or
+ // forwarded that would otherwise conflict with the @imported members.
+ for (var (module, node) in _importedModules.pairs.toList()) {
+ var shadowed = ShadowedModuleView.ifNecessary(module,
+ variables: forwardedVariableNames,
+ mixins: forwardedMixinNames,
+ functions: forwardedFunctionNames);
+ if (shadowed != null) {
+ _importedModules.remove(module);
+ if (!shadowed.isEmpty) _importedModules[shadowed] = node;
}
+ }
- for (var entry in forwardedModules.entries.toList()) {
- var module = entry.key;
- var shadowed = ShadowedModuleView.ifNecessary(module,
- variables: forwardedVariableNames,
- mixins: forwardedMixinNames,
- functions: forwardedFunctionNames);
- if (shadowed != null) {
- forwardedModules.remove(module);
- if (!shadowed.isEmpty) forwardedModules[shadowed] = entry.value;
- }
+ for (var (module, node) in forwardedModules.pairs.toList()) {
+ var shadowed = ShadowedModuleView.ifNecessary(module,
+ variables: forwardedVariableNames,
+ mixins: forwardedMixinNames,
+ functions: forwardedFunctionNames);
+ if (shadowed != null) {
+ forwardedModules.remove(module);
+ if (!shadowed.isEmpty) forwardedModules[shadowed] = node;
}
-
- _importedModules.addAll(forwarded);
- forwardedModules.addAll(forwarded);
- } else {
- (_nestedForwardedModules ??=
- List.generate(_variables.length - 1, (_) => []))
- .last
- .addAll(forwarded.keys);
}
- // Remove existing member definitions that are now shadowed by the
- // forwarded modules.
- for (var variable in forwardedVariableNames) {
- _variableIndices.remove(variable);
- _variables.last.remove(variable);
- _variableNodes.last.remove(variable);
- }
- for (var function in forwardedFunctionNames) {
- _functionIndices.remove(function);
- _functions.last.remove(function);
- }
- for (var mixin in forwardedMixinNames) {
- _mixinIndices.remove(mixin);
- _mixins.last.remove(mixin);
- }
+ _importedModules.addAll(forwarded);
+ forwardedModules.addAll(forwarded);
+ } else {
+ (_nestedForwardedModules ??=
+ List.generate(_variables.length - 1, (_) => []))
+ .last
+ .addAll(forwarded.keys);
+ }
+
+ // Remove existing member definitions that are now shadowed by the
+ // forwarded modules.
+ for (var variable in forwardedVariableNames) {
+ _variableIndices.remove(variable);
+ _variables.last.remove(variable);
+ _variableNodes.last.remove(variable);
+ }
+ for (var function in forwardedFunctionNames) {
+ _functionIndices.remove(function);
+ _functions.last.remove(function);
+ }
+ for (var mixin in forwardedMixinNames) {
+ _mixinIndices.remove(mixin);
+ _mixins.last.remove(mixin);
}
}
@@ -413,25 +414,21 @@ class AsyncEnvironment {
_getVariableFromGlobalModule(name);
}
- var index = _variableIndices[name];
- if (index != null) {
+ if (_variableIndices[name] case var index?) {
_lastVariableName = name;
_lastVariableIndex = index;
return _variables[index][name] ?? _getVariableFromGlobalModule(name);
- }
-
- index = _variableIndex(name);
- if (index == null) {
+ } else if (_variableIndex(name) case var index?) {
+ _lastVariableName = name;
+ _lastVariableIndex = index;
+ _variableIndices[name] = index;
+ return _variables[index][name] ?? _getVariableFromGlobalModule(name);
+ } else {
// There isn't a real variable defined as this index, but it will cause
// [getVariable] to short-circuit and get to this function faster next
// time the variable is accessed.
return _getVariableFromGlobalModule(name);
}
-
- _lastVariableName = name;
- _lastVariableIndex = index;
- _variableIndices[name] = index;
- return _variables[index][name] ?? _getVariableFromGlobalModule(name);
}
/// Returns the value of the variable named [name] from a namespaceless
@@ -456,22 +453,20 @@ class AsyncEnvironment {
_getVariableNodeFromGlobalModule(name);
}
- var index = _variableIndices[name];
- if (index != null) {
+ if (_variableIndices[name] case var index?) {
_lastVariableName = name;
_lastVariableIndex = index;
return _variableNodes[index][name] ??
_getVariableNodeFromGlobalModule(name);
+ } else if (_variableIndex(name) case var index?) {
+ _lastVariableName = name;
+ _lastVariableIndex = index;
+ _variableIndices[name] = index;
+ return _variableNodes[index][name] ??
+ _getVariableNodeFromGlobalModule(name);
+ } else {
+ return _getVariableNodeFromGlobalModule(name);
}
-
- index = _variableIndex(name);
- if (index == null) return _getVariableNodeFromGlobalModule(name);
-
- _lastVariableName = name;
- _lastVariableIndex = index;
- _variableIndices[name] = index;
- return _variableNodes[index][name] ??
- _getVariableNodeFromGlobalModule(name);
}
/// Returns the node for the variable named [name] from a namespaceless
@@ -486,8 +481,7 @@ class AsyncEnvironment {
// We don't need to worry about multiple modules defining the same variable,
// because that's already been checked by [getVariable].
for (var module in _importedModules.keys.followedBy(_globalModules.keys)) {
- var value = module.variableNodes[name];
- if (value != null) return value;
+ if (module.variableNodes[name] case var value?) return value;
}
return null;
}
@@ -621,16 +615,14 @@ class AsyncEnvironment {
AsyncCallable? getFunction(String name, {String? namespace}) {
if (namespace != null) return _getModule(namespace).functions[name];
- var index = _functionIndices[name];
- if (index != null) {
+ if (_functionIndices[name] case var index?) {
+ return _functions[index][name] ?? _getFunctionFromGlobalModule(name);
+ } else if (_functionIndex(name) case var index?) {
+ _functionIndices[name] = index;
return _functions[index][name] ?? _getFunctionFromGlobalModule(name);
+ } else {
+ return _getFunctionFromGlobalModule(name);
}
-
- index = _functionIndex(name);
- if (index == null) return _getFunctionFromGlobalModule(name);
-
- _functionIndices[name] = index;
- return _functions[index][name] ?? _getFunctionFromGlobalModule(name);
}
/// Returns the value of the function named [name] from a namespaceless
@@ -670,16 +662,14 @@ class AsyncEnvironment {
AsyncCallable? getMixin(String name, {String? namespace}) {
if (namespace != null) return _getModule(namespace).mixins[name];
- var index = _mixinIndices[name];
- if (index != null) {
+ if (_mixinIndices[name] case var index?) {
+ return _mixins[index][name] ?? _getMixinFromGlobalModule(name);
+ } else if (_mixinIndex(name) case var index?) {
+ _mixinIndices[name] = index;
return _mixins[index][name] ?? _getMixinFromGlobalModule(name);
+ } else {
+ return _getMixinFromGlobalModule(name);
}
-
- index = _mixinIndex(name);
- if (index == null) return _getMixinFromGlobalModule(name);
-
- _mixinIndices[name] = index;
- return _mixins[index][name] ?? _getMixinFromGlobalModule(name);
}
/// Returns the value of the mixin named [name] from a namespaceless
@@ -791,22 +781,24 @@ class AsyncEnvironment {
for (var i = 0; i < _variables.length; i++) {
var values = _variables[i];
var nodes = _variableNodes[i];
- for (var entry in values.entries) {
+ for (var (name, value) in values.pairs) {
// Implicit configurations are never invalid, making [configurationSpan]
// unnecessary, so we pass null here to avoid having to compute it.
- configuration[entry.key] =
- ConfiguredValue.implicit(entry.value, nodes[entry.key]!);
+ configuration[name] = ConfiguredValue.implicit(value, nodes[name]!);
}
}
return Configuration.implicit(configuration);
}
/// Returns a module that represents the top-level members defined in [this],
- /// that contains [css] as its CSS tree, which can be extended using
- /// [extensionStore].
- Module toModule(CssStylesheet css, ExtensionStore extensionStore) {
+ /// that contains [css] and [preModuleComments] as its CSS, which can be
+ /// extended using [extensionStore].
+ Module toModule(
+ CssStylesheet css,
+ Map> preModuleComments,
+ ExtensionStore extensionStore) {
assert(atRoot);
- return _EnvironmentModule(this, css, extensionStore,
+ return _EnvironmentModule(this, css, preModuleComments, extensionStore,
forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules)));
}
@@ -816,21 +808,18 @@ class AsyncEnvironment {
/// This is used when resolving imports, since they need to inject forwarded
/// members into the current scope. It's the only situation in which a nested
/// environment can become a module.
- Module toDummyModule() {
- return _EnvironmentModule(
- this,
- CssStylesheet(const [],
- SourceFile.decoded(const [], url: "").span(0)),
- ExtensionStore.empty,
- forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules)));
- }
+ Module toDummyModule() => _EnvironmentModule(
+ this,
+ CssStylesheet(const [],
+ SourceFile.decoded(const [], url: "").span(0)),
+ const {},
+ ExtensionStore.empty,
+ forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules)));
/// Returns the module with the given [namespace], or throws a
/// [SassScriptException] if none exists.
Module _getModule(String namespace) {
- var module = _modules[namespace];
- if (module != null) return module;
-
+ if (_modules[namespace] case var module?) return module;
throw SassScriptException(
'There is no module with the namespace "$namespace".');
}
@@ -847,18 +836,15 @@ class AsyncEnvironment {
/// The [type] should be the singular name of the value type being returned.
/// It's used to format an appropriate error message.
T? _fromOneModule(String name, String type, T? callback(Module module)) {
- var nestedForwardedModules = _nestedForwardedModules;
- if (nestedForwardedModules != null) {
+ if (_nestedForwardedModules case var nestedForwardedModules?) {
for (var modules in nestedForwardedModules.reversed) {
for (var module in modules.reversed) {
- var value = callback(module);
- if (value != null) return value;
+ if (callback(module) case var value?) return value;
}
}
}
for (var module in _importedModules.keys) {
- var value = callback(module);
- if (value != null) return value;
+ if (callback(module) case var value?) return value;
}
T? value;
@@ -873,14 +859,11 @@ class AsyncEnvironment {
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'
+ for (var (module, node) in _globalModules.pairs)
+ if (callback(module) != null) node.span: 'includes $type'
});
}
@@ -892,7 +875,7 @@ class AsyncEnvironment {
}
/// A module that represents the top-level members defined in an [Environment].
-class _EnvironmentModule implements Module {
+final class _EnvironmentModule implements Module {
Uri? get url => css.span.sourceUrl;
final List upstream;
@@ -902,6 +885,7 @@ class _EnvironmentModule implements Module {
final Map mixins;
final ExtensionStore extensionStore;
final CssStylesheet css;
+ final Map> preModuleComments;
final bool transitivelyContainsCss;
final bool transitivelyContainsExtensions;
@@ -916,13 +900,20 @@ class _EnvironmentModule implements Module {
/// defined at all.
final Map _modulesByVariable;
- factory _EnvironmentModule(AsyncEnvironment environment, CssStylesheet css,
+ factory _EnvironmentModule(
+ AsyncEnvironment environment,
+ CssStylesheet css,
+ Map> preModuleComments,
ExtensionStore extensionStore,
{Set? forwarded}) {
forwarded ??= const {};
return _EnvironmentModule._(
environment,
css,
+ Map.unmodifiable({
+ for (var (module, comments) in preModuleComments.pairs)
+ module: List.unmodifiable(comments)
+ }),
extensionStore,
_makeModulesByVariable(forwarded),
_memberMap(environment._variables.first,
@@ -934,6 +925,7 @@ class _EnvironmentModule implements Module {
_memberMap(environment._mixins.first,
forwarded.map((module) => module.mixins)),
transitivelyContainsCss: css.children.isNotEmpty ||
+ preModuleComments.isNotEmpty ||
environment._allModules
.any((module) => module.transitivelyContainsCss),
transitivelyContainsExtensions: !extensionStore.isEmpty ||
@@ -981,6 +973,7 @@ class _EnvironmentModule implements Module {
_EnvironmentModule._(
this._environment,
this.css,
+ this.preModuleComments,
this.extensionStore,
this._modulesByVariable,
this.variables,
@@ -992,8 +985,7 @@ class _EnvironmentModule implements Module {
: upstream = _environment._allModules;
void setVariable(String name, Value value, AstNode nodeWithSpan) {
- var module = _modulesByVariable[name];
- if (module != null) {
+ if (_modulesByVariable[name] case var module?) {
module.setVariable(name, value, nodeWithSpan);
return;
}
@@ -1016,11 +1008,13 @@ class _EnvironmentModule implements Module {
Module cloneCss() {
if (!transitivelyContainsCss) return this;
- var newCssAndExtensionStore = cloneCssStylesheet(css, extensionStore);
+ var (newStylesheet, newExtensionStore) =
+ cloneCssStylesheet(css, extensionStore);
return _EnvironmentModule._(
_environment,
- newCssAndExtensionStore.item1,
- newCssAndExtensionStore.item2,
+ newStylesheet,
+ preModuleComments,
+ newExtensionStore,
_modulesByVariable,
variables,
variableNodes,
diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart
index 7139b3b75..c67f77081 100644
--- a/lib/src/async_import_cache.dart
+++ b/lib/src/async_import_cache.dart
@@ -6,20 +6,31 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';
import 'package:path/path.dart' as p;
-import 'package:tuple/tuple.dart';
import 'ast/sass.dart';
+import 'deprecation.dart';
import 'importer.dart';
+import 'importer/no_op.dart';
import 'importer/utils.dart';
import 'io.dart';
import 'logger.dart';
+import 'util/nullable.dart';
import 'utils.dart';
+/// A canonicalized URL and the importer that canonicalized it.
+///
+/// This also includes the URL that was originally passed to the importer, which
+/// may be resolved relative to a base URL.
+typedef AsyncCanonicalizeResult = (
+ AsyncImporter,
+ Uri canonicalUrl, {
+ Uri originalUrl
+});
+
/// An in-memory cache of parsed stylesheets that have been imported by Sass.
///
/// {@category Dependencies}
-@sealed
-class AsyncImportCache {
+final class AsyncImportCache {
/// The importers to use when loading new Sass files.
final List _importers;
@@ -28,16 +39,14 @@ class AsyncImportCache {
/// The canonicalized URLs for each non-canonical URL.
///
- /// The second item in each key's tuple is true when this canonicalization is
- /// for an `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
- ///
- /// This map's values are the same as the return value of [canonicalize].
+ /// The `forImport` in each key is true when this canonicalization is for an
+ /// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
///
/// This cache isn't used for relative imports, because they depend on the
/// specific base importer. That's stored separately in
/// [_relativeCanonicalizeCache].
final _canonicalizeCache =
- , Tuple3?>{};
+ <(Uri, {bool forImport}), AsyncCanonicalizeResult?>{};
/// The canonicalized URLs for each non-canonical URL that's resolved using a
/// relative importer.
@@ -50,8 +59,13 @@ class AsyncImportCache {
/// 4. The `baseUrl` passed to [canonicalize].
///
/// The map's values are the same as the return value of [canonicalize].
- final _relativeCanonicalizeCache = ,
- Tuple3?>{};
+ final _relativeCanonicalizeCache = <(
+ Uri, {
+ bool forImport,
+ AsyncImporter baseImporter,
+ Uri? baseUrl
+ }),
+ AsyncCanonicalizeResult?>{};
/// The parsed stylesheets for each canonicalized import URL.
final _importCache = {};
@@ -94,6 +108,7 @@ class AsyncImportCache {
static List _toImporters(Iterable? importers,
Iterable? loadPaths, PackageConfig? packageConfig) {
var sassPath = getEnvironmentVariable('SASS_PATH');
+ if (isBrowser) return [...?importers];
return [
...?importers,
if (loadPaths != null)
@@ -117,28 +132,40 @@ class AsyncImportCache {
/// If any importers understand [url], returns that importer as well as the
/// canonicalized URL and the original URL (resolved relative to [baseUrl] if
/// applicable). Otherwise, returns `null`.
- Future?> canonicalize(Uri url,
+ Future canonicalize(Uri url,
{AsyncImporter? baseImporter,
Uri? baseUrl,
bool forImport = false}) async {
+ if (isBrowser &&
+ (baseImporter == null || baseImporter is NoOpImporter) &&
+ _importers.isEmpty) {
+ throw "Custom importers are required to load stylesheets when compiling in the browser.";
+ }
+
if (baseImporter != null) {
- var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache,
- Tuple4(url, forImport, baseImporter, baseUrl), () async {
+ var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, (
+ url,
+ forImport: forImport,
+ baseImporter: baseImporter,
+ baseUrl: baseUrl
+ ), () async {
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
- var canonicalUrl =
- await _canonicalize(baseImporter, resolvedUrl, forImport);
- if (canonicalUrl == null) return null;
- return Tuple3(baseImporter, canonicalUrl, resolvedUrl);
+ if (await _canonicalize(baseImporter, resolvedUrl, forImport)
+ case var canonicalUrl?) {
+ return (baseImporter, canonicalUrl, originalUrl: resolvedUrl);
+ } else {
+ return null;
+ }
});
if (relativeResult != null) return relativeResult;
}
- return await putIfAbsentAsync(_canonicalizeCache, Tuple2(url, forImport),
- () async {
+ return await putIfAbsentAsync(
+ _canonicalizeCache, (url, forImport: forImport), () async {
for (var importer in _importers) {
- var canonicalUrl = await _canonicalize(importer, url, forImport);
- if (canonicalUrl != null) {
- return Tuple3(importer, canonicalUrl, url);
+ if (await _canonicalize(importer, url, forImport)
+ case var canonicalUrl?) {
+ return (importer, canonicalUrl, originalUrl: url);
}
}
@@ -154,10 +181,10 @@ class AsyncImportCache {
? inImportRule(() => importer.canonicalize(url))
: importer.canonicalize(url));
if (result?.scheme == '') {
- _logger.warn("""
+ _logger.warnForDeprecation(Deprecation.relativeCanonical, """
Importer $importer canonicalized $url to $result.
Relative canonical URLs are deprecated and will eventually be disallowed.
-""", deprecation: true);
+""");
}
return result;
}
@@ -171,17 +198,19 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
/// parsed stylesheet. Otherwise, returns `null`.
///
/// Caches the result of the import and uses cached results if possible.
- Future?> import(Uri url,
+ Future<(AsyncImporter, Stylesheet)?> import(Uri url,
{AsyncImporter? baseImporter,
Uri? baseUrl,
bool forImport = false}) async {
- var tuple = await canonicalize(url,
- baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport);
- if (tuple == null) return null;
- var stylesheet = await importCanonical(tuple.item1, tuple.item2,
- originalUrl: tuple.item3);
- if (stylesheet == null) return null;
- return Tuple2(tuple.item1, stylesheet);
+ if (await canonicalize(url,
+ baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport)
+ case (var importer, var canonicalUrl, :var originalUrl)) {
+ return (await importCanonical(importer, canonicalUrl,
+ originalUrl: originalUrl))
+ .andThen((stylesheet) => (importer, stylesheet));
+ } else {
+ return null;
+ }
}
/// Tries to load the canonicalized [canonicalUrl] using [importer].
@@ -217,21 +246,22 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
/// Return a human-friendly URL for [canonicalUrl] to use in a stack trace.
///
/// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache.
- Uri humanize(Uri canonicalUrl) {
- // Display the URL with the shortest path length.
- var url = minBy(
- _canonicalizeCache.values
- .whereNotNull()
- .where((tuple) => tuple.item2 == canonicalUrl)
- .map((tuple) => tuple.item3),
- (url) => url.path.length);
- if (url == null) return canonicalUrl;
-
- // Use the canonicalized basename so that we display e.g.
- // package:example/_example.scss rather than package:example/example in
- // stack traces.
- return url.resolve(p.url.basename(canonicalUrl.path));
- }
+ Uri humanize(Uri canonicalUrl) =>
+ // If multiple original URLs canonicalize to the same thing, choose the
+ // shortest one.
+ minBy(
+ _canonicalizeCache.values
+ .whereNotNull()
+ .where((result) => result.$2 == canonicalUrl)
+ .map((result) => result.originalUrl),
+ (url) => url.path.length)
+ // Use the canonicalized basename so that we display e.g.
+ // package:example/_example.scss rather than package:example/example
+ // in stack traces.
+ .andThen((url) => url.resolve(p.url.basename(canonicalUrl.path))) ??
+ // If we don't have an original URL cached, display the canonical URL
+ // as-is.
+ canonicalUrl;
/// Returns the URL to use in the source map to refer to [canonicalUrl].
///
@@ -246,16 +276,9 @@ Relative canonical URLs are deprecated and will eventually be disallowed.
/// @nodoc
@internal
void clearCanonicalize(Uri url) {
- _canonicalizeCache.remove(Tuple2(url, false));
- _canonicalizeCache.remove(Tuple2(url, true));
-
- var relativeKeysToClear = [
- for (var key in _relativeCanonicalizeCache.keys)
- if (key.item1 == url) key
- ];
- for (var key in relativeKeysToClear) {
- _relativeCanonicalizeCache.remove(key);
- }
+ _canonicalizeCache.remove((url, forImport: false));
+ _canonicalizeCache.remove((url, forImport: true));
+ _relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url);
}
/// Clears the cached parse tree for the stylesheet with the given
diff --git a/lib/src/callable.dart b/lib/src/callable.dart
index 28f65c2b1..2d2ed1e26 100644
--- a/lib/src/callable.dart
+++ b/lib/src/callable.dart
@@ -3,9 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:meta/meta.dart';
-import 'package:tuple/tuple.dart';
-import 'ast/sass.dart';
import 'callable/async.dart';
import 'callable/built_in.dart';
import 'exception.dart';
@@ -69,7 +67,7 @@ export 'callable/user_defined.dart';
///
/// {@category Compile}
@sealed
-abstract class Callable extends AsyncCallable {
+abstract interface class Callable implements AsyncCallable {
@Deprecated('Use `Callable.function` instead.')
factory Callable(String name, String arguments,
Value callback(List arguments)) =>
@@ -127,8 +125,8 @@ abstract class Callable extends AsyncCallable {
factory Callable.fromSignature(
String signature, Value callback(List arguments),
{bool requireParens = true}) {
- Tuple2 tuple =
+ var (name, declaration) =
parseSignature(signature, requireParens: requireParens);
- return BuiltInCallable.parsed(tuple.item1, tuple.item2, callback);
+ return BuiltInCallable.parsed(name, declaration, callback);
}
}
diff --git a/lib/src/callable/async.dart b/lib/src/callable/async.dart
index 7a77d243a..433ca98b6 100644
--- a/lib/src/callable/async.dart
+++ b/lib/src/callable/async.dart
@@ -5,9 +5,7 @@
import 'dart:async';
import 'package:meta/meta.dart';
-import 'package:tuple/tuple.dart';
-import '../ast/sass.dart';
import '../exception.dart';
import '../utils.dart';
import '../value.dart';
@@ -24,7 +22,7 @@ import 'async_built_in.dart';
///
/// {@category Compile}
@sealed
-abstract class AsyncCallable {
+abstract interface class AsyncCallable {
/// The callable's name.
String get name;
@@ -50,8 +48,8 @@ abstract class AsyncCallable {
factory AsyncCallable.fromSignature(
String signature, FutureOr callback(List arguments),
{bool requireParens = true}) {
- Tuple2 tuple =
+ var (name, declaration) =
parseSignature(signature, requireParens: requireParens);
- return AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2, callback);
+ return AsyncBuiltInCallable.parsed(name, declaration, callback);
}
}
diff --git a/lib/src/callable/async_built_in.dart b/lib/src/callable/async_built_in.dart
index ff4513f25..0132b787f 100644
--- a/lib/src/callable/async_built_in.dart
+++ b/lib/src/callable/async_built_in.dart
@@ -4,8 +4,6 @@
import 'dart:async';
-import 'package:tuple/tuple.dart';
-
import '../ast/sass.dart';
import '../value.dart';
import 'async.dart';
@@ -76,7 +74,7 @@ class AsyncBuiltInCallable implements AsyncCallable {
/// If no exact match is found, finds the closest approximation. Note that this
/// doesn't guarantee that [positional] and [names] are valid for the returned
/// [ArgumentDeclaration].
- Tuple2 callbackFor(
+ (ArgumentDeclaration, Callback) callbackFor(
int positional, Set names) =>
- Tuple2(_arguments, _callback);
+ (_arguments, _callback);
}
diff --git a/lib/src/callable/built_in.dart b/lib/src/callable/built_in.dart
index a6bad3414..905d11e56 100644
--- a/lib/src/callable/built_in.dart
+++ b/lib/src/callable/built_in.dart
@@ -2,10 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:tuple/tuple.dart';
-
import '../ast/sass.dart';
import '../callable.dart';
+import '../util/map.dart';
import '../value.dart';
typedef Callback = Value Function(List arguments);
@@ -16,11 +15,11 @@ typedef Callback = Value Function(List arguments);
/// may declare multiple different callbacks with multiple different sets of
/// arguments. When the callable is invoked, the first callback with matching
/// arguments is invoked.
-class BuiltInCallable implements Callable, AsyncBuiltInCallable {
+final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
final String name;
/// The overloads declared for this callable.
- final List> _overloads;
+ final List<(ArgumentDeclaration, Callback)> _overloads;
/// Creates a function with a single [arguments] declaration and a single
/// [callback].
@@ -61,7 +60,7 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
/// [callback].
BuiltInCallable.parsed(this.name, ArgumentDeclaration arguments,
Value callback(List arguments))
- : _overloads = [Tuple2(arguments, callback)];
+ : _overloads = [(arguments, callback)];
/// Creates a function with multiple implementations.
///
@@ -75,11 +74,11 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
BuiltInCallable.overloadedFunction(this.name, Map overloads,
{Object? url})
: _overloads = [
- for (var entry in overloads.entries)
- Tuple2(
- ArgumentDeclaration.parse('@function $name(${entry.key}) {',
- url: url),
- entry.value)
+ for (var (args, callback) in overloads.pairs)
+ (
+ ArgumentDeclaration.parse('@function $name($args) {', url: url),
+ callback
+ )
];
BuiltInCallable._(this.name, this._overloads);
@@ -90,16 +89,16 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
/// If no exact match is found, finds the closest approximation. Note that this
/// doesn't guarantee that [positional] and [names] are valid for the returned
/// [ArgumentDeclaration].
- Tuple2 callbackFor(
+ (ArgumentDeclaration, Callback) callbackFor(
int positional, Set names) {
- Tuple2? fuzzyMatch;
+ (ArgumentDeclaration, Callback)? fuzzyMatch;
int? minMismatchDistance;
for (var overload in _overloads) {
// Ideally, find an exact match.
- if (overload.item1.matches(positional, names)) return overload;
+ if (overload.$1.matches(positional, names)) return overload;
- var mismatchDistance = overload.item1.arguments.length - positional;
+ var mismatchDistance = overload.$1.arguments.length - positional;
if (minMismatchDistance != null) {
if (mismatchDistance.abs() > minMismatchDistance.abs()) continue;
diff --git a/lib/src/callable/plain_css.dart b/lib/src/callable/plain_css.dart
index 9a74ed604..cd46e1f5c 100644
--- a/lib/src/callable/plain_css.dart
+++ b/lib/src/callable/plain_css.dart
@@ -7,7 +7,7 @@ import '../callable.dart';
/// A callable that emits a plain CSS function.
///
/// This can't be used for mixins.
-class PlainCssCallable implements Callable {
+final class PlainCssCallable implements Callable {
final String name;
PlainCssCallable(this.name);
diff --git a/lib/src/callable/user_defined.dart b/lib/src/callable/user_defined.dart
index a0a2af72c..6e0ecfacc 100644
--- a/lib/src/callable/user_defined.dart
+++ b/lib/src/callable/user_defined.dart
@@ -8,7 +8,7 @@ import '../callable.dart';
/// A callback defined in the user's Sass stylesheet.
///
/// The type parameter [E] should either be `Environment` or `AsyncEnvironment`.
-class UserDefinedCallable implements Callable {
+final class UserDefinedCallable implements Callable {
/// The declaration.
final CallableDeclaration declaration;
diff --git a/lib/src/color_names.dart b/lib/src/color_names.dart
index 6bc4575b3..ae315663d 100644
--- a/lib/src/color_names.dart
+++ b/lib/src/color_names.dart
@@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'value.dart';
+import 'util/map.dart';
/// A map from (lowercase) color names to their color values.
final colorsByName = {
@@ -161,5 +162,5 @@ final colorsByName = {
/// A map from Sass colors to (lowercase) color names.
final namesByColor = {
- for (var entry in colorsByName.entries) entry.value: entry.key
+ for (var (name, color) in colorsByName.pairs) color: name
};
diff --git a/lib/src/compile.dart b/lib/src/compile.dart
index 8e4f650cb..2b24def1b 100644
--- a/lib/src/compile.dart
+++ b/lib/src/compile.dart
@@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_compile.dart.
// See tool/grind/synchronize.dart for details.
//
-// Checksum: f8b5bf7eafbe3523ca4df1a6832e131c5c03986b
+// Checksum: c2982db43bcd56f81cab3f51b5669e0edd3cfafb
//
// ignore_for_file: unused_import
@@ -19,11 +19,13 @@ import 'ast/sass.dart';
import 'import_cache.dart';
import 'callable.dart';
import 'compile_result.dart';
+import 'deprecation.dart';
import 'importer.dart';
import 'importer/legacy_node.dart';
+import 'importer/no_op.dart';
import 'io.dart';
import 'logger.dart';
-import 'logger/terse.dart';
+import 'logger/deprecation_handling.dart';
import 'syntax.dart';
import 'utils.dart';
import 'visitor/evaluate.dart';
@@ -46,9 +48,14 @@ CompileResult compile(String path,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) {
- TerseLogger? terseLogger;
- if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr());
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) {
+ DeprecationHandlingLogger deprecationLogger = logger =
+ DeprecationHandlingLogger(logger ?? Logger.stderr(),
+ fatalDeprecations: {...?fatalDeprecations},
+ futureDeprecations: {...?futureDeprecations},
+ limitRepetition: !verbose);
// If the syntax is different than the importer would default to, we have to
// parse the file manually and we can't store it in the cache.
@@ -80,7 +87,7 @@ CompileResult compile(String path,
sourceMap,
charset);
- terseLogger?.summarize(node: nodeImporter != null);
+ deprecationLogger.summarize(js: nodeImporter != null);
return result;
}
@@ -105,9 +112,14 @@ CompileResult compileString(String source,
bool quietDeps = false,
bool verbose = false,
bool sourceMap = false,
- bool charset = true}) {
- TerseLogger? terseLogger;
- if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr());
+ bool charset = true,
+ Iterable? fatalDeprecations,
+ Iterable? futureDeprecations}) {
+ DeprecationHandlingLogger deprecationLogger = logger =
+ DeprecationHandlingLogger(logger ?? Logger.stderr(),
+ fatalDeprecations: {...?fatalDeprecations},
+ futureDeprecations: {...?futureDeprecations},
+ limitRepetition: !verbose);
var stylesheet =
Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger);
@@ -117,7 +129,7 @@ CompileResult compileString(String source,
logger,
importCache,
nodeImporter,
- importer ?? FilesystemImporter('.'),
+ importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')),
functions,
style,
useSpaces,
@@ -127,7 +139,7 @@ CompileResult compileString(String source,
sourceMap,
charset);
- terseLogger?.summarize(node: nodeImporter != null);
+ deprecationLogger.summarize(js: nodeImporter != null);
return result;
}
@@ -167,9 +179,7 @@ CompileResult _compileStylesheet(
var resultSourceMap = serializeResult.sourceMap;
if (resultSourceMap != null && importCache != null) {
- // TODO(nweiz): Don't explicitly use a type parameter when dart-lang/sdk#25490
- // is fixed.
- mapInPlace(
+ mapInPlace(
resultSourceMap.urls,
(url) => url == ''
? Uri.dataFromString(stylesheet.span.file.getText(0),
diff --git a/lib/src/compile_result.dart b/lib/src/compile_result.dart
index 459c899dc..ad3e60c0d 100644
--- a/lib/src/compile_result.dart
+++ b/lib/src/compile_result.dart
@@ -21,7 +21,7 @@ class CompileResult {
final SerializeResult _serialize;
/// The compiled CSS.
- String get css => _serialize.css;
+ String get css => _serialize.$1;
/// The source map indicating how the source files map to [css].
///
diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart
index b8f10a5bb..1a65d236a 100644
--- a/lib/src/configuration.dart
+++ b/lib/src/configuration.dart
@@ -7,6 +7,7 @@ import 'ast/node.dart';
import 'ast/sass.dart';
import 'configured_value.dart';
import 'util/limited_map_view.dart';
+import 'util/map.dart';
import 'util/unprefixed_map_view.dart';
/// A set of variables meant to configure a module by overriding its
@@ -17,7 +18,7 @@ import 'util/unprefixed_map_view.dart';
/// meaning that it's created by passing a `with` clause to a `@use` rule.
/// Explicit configurations have spans associated with them and are represented
/// by the [ExplicitConfiguration] subclass.
-class Configuration {
+final class Configuration {
/// A map from variable names (without `$`) to values.
///
/// This map may not be modified directly. To remove a value from this
@@ -76,14 +77,14 @@ class Configuration {
// 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.
- var prefix = forward.prefix;
- if (prefix != null) newValues = UnprefixedMapView(newValues, prefix);
+ if (forward.prefix case var prefix?) {
+ newValues = UnprefixedMapView(newValues, prefix);
+ }
- var shownVariables = forward.shownVariables;
- var hiddenVariables = forward.hiddenVariables;
- if (shownVariables != null) {
+ if (forward.shownVariables case var shownVariables?) {
newValues = LimitedMapView.safelist(newValues, shownVariables);
- } else if (hiddenVariables != null && hiddenVariables.isNotEmpty) {
+ } else if (forward.hiddenVariables case var hiddenVariables?
+ when hiddenVariables.isNotEmpty) {
newValues = LimitedMapView.blocklist(newValues, hiddenVariables);
}
return _withValues(newValues);
@@ -101,9 +102,7 @@ class Configuration {
String toString() =>
"(" +
- values.entries
- .map((entry) => "\$${entry.key}: ${entry.value}")
- .join(", ") +
+ [for (var (name, value) in values.pairs) "\$$name: $value"].join(",") +
")";
}
@@ -114,7 +113,7 @@ class Configuration {
/// configurations will cause an error if attempting to use them on a module
/// that has already been loaded, while implicit configurations will be
/// silently ignored in this case.
-class ExplicitConfiguration extends Configuration {
+final class ExplicitConfiguration extends Configuration {
/// The node whose span indicates where the configuration was declared.
final AstNode nodeWithSpan;
diff --git a/lib/src/configured_value.dart b/lib/src/configured_value.dart
index c373b1f8e..faf969cad 100644
--- a/lib/src/configured_value.dart
+++ b/lib/src/configured_value.dart
@@ -8,7 +8,7 @@ import 'ast/node.dart';
import 'value.dart';
/// A variable value that's been configured for a [Configuration].
-class ConfiguredValue {
+final class ConfiguredValue {
/// The value of the variable.
final Value value;
diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart
new file mode 100644
index 000000000..0266e30e0
--- /dev/null
+++ b/lib/src/deprecation.dart
@@ -0,0 +1,136 @@
+// Copyright 2022 Google LLC. Use of this source code is governed by an
+// MIT-style license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:collection/collection.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'io.dart';
+import 'util/nullable.dart';
+
+/// A deprecated feature in the language.
+enum Deprecation {
+ /// Deprecation for passing a string to `call` instead of `get-function`.
+ callString('call-string',
+ deprecatedIn: '0.0.0',
+ description: 'Passing a string directly to meta.call().'),
+
+ /// Deprecation for `@elseif`.
+ elseif('elseif', deprecatedIn: '1.3.2', description: '@elseif.'),
+
+ /// Deprecation for parsing `@-moz-document`.
+ mozDocument('moz-document',
+ deprecatedIn: '1.7.2', description: '@-moz-document.'),
+
+ /// Deprecation for importers using relative canonical URLs.
+ relativeCanonical('relative-canonical', deprecatedIn: '1.14.2'),
+
+ /// Deprecation for declaring new variables with `!global`.
+ newGlobal('new-global',
+ deprecatedIn: '1.17.2',
+ description: 'Declaring new variables with !global.'),
+
+ /// Deprecation for certain functions in the color module matching the
+ /// behavior of their global counterparts for compatiblity reasons.
+ colorModuleCompat('color-module-compat',
+ deprecatedIn: '1.23.0',
+ description:
+ 'Using color module functions in place of plain CSS functions.'),
+
+ /// Deprecation for treating `/` as division.
+ slashDiv('slash-div',
+ deprecatedIn: '1.33.0', description: '/ operator for division.'),
+
+ /// Deprecation for leading, trailing, and repeated combinators.
+ bogusCombinators('bogus-combinators',
+ deprecatedIn: '1.54.0',
+ description: 'Leading, trailing, and repeated combinators.'),
+
+ /// Deprecation for ambiguous `+` and `-` operators.
+ strictUnary('strict-unary',
+ deprecatedIn: '1.55.0', description: 'Ambiguous + and - operators.'),
+
+ /// Deprecation for passing invalid units to certain built-in functions.
+ functionUnits('function-units',
+ deprecatedIn: '1.56.0',
+ description: 'Passing invalid units to built-in functions.'),
+
+ /// Deprecation for passing percentages to the Sass abs() function.
+ absPercent('abs-percent',
+ deprecatedIn: '1.65.0',
+ description: 'Passing percentages to the Sass abs() function.'),
+
+ duplicateVariableFlags('duplicate-var-flags',
+ deprecatedIn: '1.62.0',
+ description:
+ 'Using !default or !global multiple times for one variable.'),
+
+ nullAlpha('null-alpha',
+ deprecatedIn: '1.62.3',
+ description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'),
+
+ colorFunctions('color-functions',
+ deprecatedIn: '1.67.0',
+ description: 'Using global Sass color functions.'),
+
+ /// Deprecation for `@import` rules.
+ import.future('import', description: '@import rules.'),
+
+ /// Used for deprecations coming from user-authored code.
+ userAuthored('user-authored', deprecatedIn: null);
+
+ /// A unique ID for this deprecation in kebab case.
+ ///
+ /// This is used to refer to the deprecation on the command line.
+ final String id;
+
+ /// Underlying version string used by [deprecatedIn].
+ ///
+ /// This is necessary because [Version] doesn't have a constant constructor,
+ /// so we can't use it directly as an enum property.
+ final String? _deprecatedIn;
+
+ /// The Dart Sass version this feature was first deprecated in.
+ ///
+ /// For deprecations that have existed in all versions of Dart Sass, this
+ /// should be 0.0.0. For deprecations not related to a specific Sass version,
+ /// this should be null.
+ Version? get deprecatedIn => _deprecatedIn.andThen(Version.parse);
+
+ /// A description of this deprecation that will be displayed in the CLI usage.
+ ///
+ /// If this is null, the given deprecation will not be listed.
+ final String? description;
+
+ /// Whether this deprecation will occur in the future.
+ ///
+ /// If this is true, `deprecatedIn` will be null, since we do not yet know
+ /// what version of Dart Sass this deprecation will be live in.
+ final bool isFuture;
+
+ /// Constructs a regular deprecation.
+ const Deprecation(this.id, {required String? deprecatedIn, this.description})
+ : _deprecatedIn = deprecatedIn,
+ isFuture = false;
+
+ /// Constructs a future deprecation.
+ const Deprecation.future(this.id, {this.description})
+ : _deprecatedIn = null,
+ isFuture = true;
+
+ @override
+ String toString() => id;
+
+ /// Returns the deprecation with a given ID, or null if none exists.
+ static Deprecation? fromId(String id) => Deprecation.values
+ .firstWhereOrNull((deprecation) => deprecation.id == id);
+
+ /// Returns the set of all deprecations done in or before [version].
+ static Set forVersion(Version version) {
+ var range = VersionRange(max: version, includeMax: true);
+ return {
+ for (var deprecation in Deprecation.values)
+ if (deprecation.deprecatedIn.andThen(range.allows) ?? false) deprecation
+ };
+ }
+}
diff --git a/lib/src/embedded/dispatcher.dart b/lib/src/embedded/dispatcher.dart
new file mode 100644
index 000000000..fae22b458
--- /dev/null
+++ b/lib/src/embedded/dispatcher.dart
@@ -0,0 +1,321 @@
+// Copyright 2019 Google Inc. Use of this source code is governed by an
+// MIT-style license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:path/path.dart' as p;
+import 'package:protobuf/protobuf.dart';
+import 'package:sass/sass.dart' as sass;
+import 'package:stream_channel/stream_channel.dart';
+
+import 'embedded_sass.pb.dart';
+import 'function_registry.dart';
+import 'host_callable.dart';
+import 'importer/file.dart';
+import 'importer/host.dart';
+import 'logger.dart';
+import 'util/proto_extensions.dart';
+import 'utils.dart';
+
+/// The request ID used for all outbound requests.
+///
+/// Since the dispatcher runs a single-threaded compilation, it will only ever
+/// have one active request at a time, so there's no need to vary the ID.
+final _outboundRequestId = 0;
+
+/// A class that dispatches messages to and from the host for a single
+/// compilation.
+final class Dispatcher {
+ /// The channel of encoded protocol buffers, connected to the host.
+ final StreamChannel _channel;
+
+ /// The compilation ID for which this dispatcher is running.
+ ///
+ /// This is added to outgoing messages but is _not_ parsed from incoming
+ /// messages, since that's already handled by the [IsolateDispatcher].
+ final int _compilationId;
+
+ /// [_compilationId], serialized as a varint.
+ final Uint8List _compilationIdVarint;
+
+ /// Whether this dispatcher has received its compile request.
+ var _compiling = false;
+
+ /// A completer awaiting a response to an outbound request.
+ ///
+ /// Since each [Dispatcher] is only running a single-threaded compilation, it
+ /// can only ever have one request outstanding.
+ Completer? _outstandingRequest;
+
+ /// Creates a [Dispatcher] that sends and receives encoded protocol buffers
+ /// over [channel].
+ Dispatcher(this._channel, this._compilationId)
+ : _compilationIdVarint = serializeVarint(_compilationId);
+
+ /// Listens for incoming `CompileRequests` and runs their compilations.
+ ///
+ /// This may only be called once. Returns whether or not the compilation
+ /// succeeded.
+ Future listen() async {
+ var success = false;
+ await _channel.stream.listen((binaryMessage) async {
+ // Wait a single microtask tick so that we're running in a separate
+ // microtask from the initial request dispatch. Otherwise, [waitFor] will
+ // deadlock the event loop fiber that would otherwise be checking stdin
+ // for new input.
+ await Future.value();
+
+ try {
+ InboundMessage? message;
+ try {
+ message = InboundMessage.fromBuffer(binaryMessage);
+ } on InvalidProtocolBufferException catch (error) {
+ throw parseError(error.message);
+ }
+
+ switch (message.whichMessage()) {
+ case InboundMessage_Message.versionRequest:
+ throw paramsError("VersionRequest must have compilation ID 0.");
+
+ case InboundMessage_Message.compileRequest:
+ if (_compiling) {
+ throw paramsError(
+ "A CompileRequest with compilation ID $_compilationId is "
+ "already active.");
+ }
+ _compiling = true;
+
+ var request = message.compileRequest;
+ var response = await _compile(request);
+ _send(OutboundMessage()..compileResponse = response);
+ success = true;
+ // Each Dispatcher runs a single compilation and then closes.
+ _channel.sink.close();
+
+ case InboundMessage_Message.canonicalizeResponse:
+ _dispatchResponse(message.id, message.canonicalizeResponse);
+
+ case InboundMessage_Message.importResponse:
+ _dispatchResponse(message.id, message.importResponse);
+
+ case InboundMessage_Message.fileImportResponse:
+ _dispatchResponse(message.id, message.fileImportResponse);
+
+ case InboundMessage_Message.functionCallResponse:
+ _dispatchResponse(message.id, message.functionCallResponse);
+
+ case InboundMessage_Message.notSet:
+ throw parseError("InboundMessage.message is not set.");
+
+ default:
+ throw parseError(
+ "Unknown message type: ${message.toDebugString()}");
+ }
+ } on ProtocolError catch (error, stackTrace) {
+ sendError(handleError(error, stackTrace));
+ _channel.sink.close();
+ }
+ }).asFuture();
+ return success;
+ }
+
+ Future _compile(
+ InboundMessage_CompileRequest request) async {
+ var functions = FunctionRegistry();
+
+ var style = request.style == OutputStyle.COMPRESSED
+ ? sass.OutputStyle.compressed
+ : sass.OutputStyle.expanded;
+ var logger = EmbeddedLogger(this,
+ color: request.alertColor, ascii: request.alertAscii);
+
+ try {
+ var importers = request.importers.map((importer) =>
+ _decodeImporter(request, importer) ??
+ (throw mandatoryError("Importer.importer")));
+
+ var globalFunctions = request.globalFunctions
+ .map((signature) => hostCallable(this, functions, signature));
+
+ late sass.CompileResult result;
+ switch (request.whichInput()) {
+ case InboundMessage_CompileRequest_Input.string:
+ var input = request.string;
+ result = sass.compileStringToResult(input.source,
+ color: request.alertColor,
+ logger: logger,
+ importers: importers,
+ importer: _decodeImporter(request, input.importer) ??
+ (input.url.startsWith("file:") ? null : sass.Importer.noOp),
+ functions: globalFunctions,
+ syntax: syntaxToSyntax(input.syntax),
+ style: style,
+ url: input.url.isEmpty ? null : input.url,
+ quietDeps: request.quietDeps,
+ verbose: request.verbose,
+ sourceMap: request.sourceMap,
+ charset: request.charset);
+ break;
+
+ case InboundMessage_CompileRequest_Input.path:
+ if (request.path.isEmpty) {
+ throw mandatoryError("CompileRequest.Input.path");
+ }
+
+ try {
+ result = sass.compileToResult(request.path,
+ color: request.alertColor,
+ logger: logger,
+ importers: importers,
+ functions: globalFunctions,
+ style: style,
+ quietDeps: request.quietDeps,
+ verbose: request.verbose,
+ sourceMap: request.sourceMap,
+ charset: request.charset);
+ } on FileSystemException catch (error) {
+ return OutboundMessage_CompileResponse()
+ ..failure = (OutboundMessage_CompileResponse_CompileFailure()
+ ..message = error.path == null
+ ? error.message
+ : "${error.message}: ${error.path}"
+ ..span = (SourceSpan()
+ ..start = SourceSpan_SourceLocation()
+ ..end = SourceSpan_SourceLocation()
+ ..url = p.toUri(request.path).toString()));
+ }
+ break;
+
+ case InboundMessage_CompileRequest_Input.notSet:
+ throw mandatoryError("CompileRequest.input");
+ }
+
+ var success = OutboundMessage_CompileResponse_CompileSuccess()
+ ..css = result.css;
+
+ var sourceMap = result.sourceMap;
+ if (sourceMap != null) {
+ success.sourceMap = json.encode(sourceMap.toJson(
+ includeSourceContents: request.sourceMapIncludeSources));
+ }
+ return OutboundMessage_CompileResponse()
+ ..success = success
+ ..loadedUrls.addAll(result.loadedUrls.map((url) => url.toString()));
+ } on sass.SassException catch (error) {
+ var formatted = withGlyphs(
+ () => error.toString(color: request.alertColor),
+ ascii: request.alertAscii);
+ return OutboundMessage_CompileResponse()
+ ..failure = (OutboundMessage_CompileResponse_CompileFailure()
+ ..message = error.message
+ ..span = protofySpan(error.span)
+ ..stackTrace = error.trace.toString()
+ ..formatted = formatted)
+ ..loadedUrls.addAll(error.loadedUrls.map((url) => url.toString()));
+ }
+ }
+
+ /// Converts [importer] into a [sass.Importer].
+ sass.Importer? _decodeImporter(InboundMessage_CompileRequest request,
+ InboundMessage_CompileRequest_Importer importer) {
+ switch (importer.whichImporter()) {
+ case InboundMessage_CompileRequest_Importer_Importer.path:
+ return sass.FilesystemImporter(importer.path);
+
+ case InboundMessage_CompileRequest_Importer_Importer.importerId:
+ return HostImporter(this, importer.importerId);
+
+ case InboundMessage_CompileRequest_Importer_Importer.fileImporterId:
+ return FileImporter(this, importer.fileImporterId);
+
+ case InboundMessage_CompileRequest_Importer_Importer.notSet:
+ return null;
+ }
+ }
+
+ /// Sends [event] to the host.
+ void sendLog(OutboundMessage_LogEvent event) =>
+ _send(OutboundMessage()..logEvent = event);
+
+ /// Sends [error] to the host.
+ void sendError(ProtocolError error) =>
+ _send(OutboundMessage()..error = error);
+
+ Future sendCanonicalizeRequest(
+ OutboundMessage_CanonicalizeRequest request) =>
+ _sendRequest(
+ OutboundMessage()..canonicalizeRequest = request);
+
+ Future sendImportRequest(
+ OutboundMessage_ImportRequest request) =>
+ _sendRequest(
+ OutboundMessage()..importRequest = request);
+
+ Future sendFileImportRequest(
+ OutboundMessage_FileImportRequest request) =>
+ _sendRequest(
+ OutboundMessage()..fileImportRequest = request);
+
+ Future sendFunctionCallRequest(
+ OutboundMessage_FunctionCallRequest request) =>
+ _sendRequest(
+ OutboundMessage()..functionCallRequest = request);
+
+ /// Sends [request] to the host and returns the message sent in response.
+ Future _sendRequest(
+ OutboundMessage request) async {
+ request.id = _outboundRequestId;
+ _send(request);
+
+ if (_outstandingRequest != null) {
+ throw StateError(
+ "Dispatcher.sendRequest() can't be called when another request is "
+ "active.");
+ }
+
+ return (_outstandingRequest = Completer