Repository: servo/stylo Branch: main Commit: 71c42fd1ed99 Files: 375 Total size: 5.1 MB Directory structure: gitextract_8xmmvn70/ ├── .github/ │ └── workflows/ │ ├── main.yml │ ├── mirror-to-release-branch.yml │ └── sync-upstream.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── SYNCING.md ├── commit-from-merge.sh ├── commit-from-squashed.sh ├── malloc_size_of/ │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── lib.rs ├── rustfmt.toml ├── selectors/ │ ├── CHANGES.md │ ├── Cargo.toml │ ├── README.md │ ├── attr.rs │ ├── bloom.rs │ ├── build.rs │ ├── builder.rs │ ├── context.rs │ ├── kleene_value.rs │ ├── lib.rs │ ├── matching.rs │ ├── nth_index_cache.rs │ ├── parser.rs │ ├── relative_selector/ │ │ ├── cache.rs │ │ ├── filter.rs │ │ └── mod.rs │ ├── sink.rs │ ├── tree.rs │ └── visitor.rs ├── servo_arc/ │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ └── lib.rs ├── shell.nix ├── start-rebase.sh ├── style/ │ ├── Cargo.toml │ ├── README.md │ ├── applicable_declarations.rs │ ├── author_styles.rs │ ├── bezier.rs │ ├── bloom.rs │ ├── build.rs │ ├── build_gecko.rs │ ├── color/ │ │ ├── color_function.rs │ │ ├── component.rs │ │ ├── convert.rs │ │ ├── mix.rs │ │ ├── mod.rs │ │ ├── parsing.rs │ │ └── to_css.rs │ ├── context.rs │ ├── counter_style/ │ │ ├── mod.rs │ │ ├── predefined.rs │ │ └── update_predefined.py │ ├── custom_properties.rs │ ├── custom_properties_map.rs │ ├── data.rs │ ├── device/ │ │ ├── gecko.rs │ │ ├── mod.rs │ │ └── servo.rs │ ├── dom.rs │ ├── dom_apis.rs │ ├── driver.rs │ ├── error_reporting.rs │ ├── font_face.rs │ ├── font_metrics.rs │ ├── gecko/ │ │ ├── anon_boxes.toml │ │ ├── arc_types.rs │ │ ├── conversions.rs │ │ ├── data.rs │ │ ├── media_features.rs │ │ ├── mod.rs │ │ ├── non_ts_pseudo_class_list.rs │ │ ├── pseudo_element.rs │ │ ├── pseudo_element_definition.mako.rs │ │ ├── pseudo_elements.py │ │ ├── pseudo_elements.toml │ │ ├── regen_atoms.py │ │ ├── restyle_damage.rs │ │ ├── selector_parser.rs │ │ ├── snapshot.rs │ │ ├── snapshot_helpers.rs │ │ ├── traversal.rs │ │ ├── url.rs │ │ └── wrapper.rs │ ├── gecko_bindings/ │ │ ├── mod.rs │ │ └── sugar/ │ │ ├── mod.rs │ │ ├── ns_com_ptr.rs │ │ ├── ns_compatibility.rs │ │ ├── ns_style_auto_array.rs │ │ ├── origin_flags.rs │ │ ├── ownership.rs │ │ └── refptr.rs │ ├── gecko_string_cache/ │ │ ├── mod.rs │ │ └── namespace.rs │ ├── global_style_data.rs │ ├── invalidation/ │ │ ├── element/ │ │ │ ├── document_state.rs │ │ │ ├── element_wrapper.rs │ │ │ ├── invalidation_map.rs │ │ │ ├── invalidator.rs │ │ │ ├── mod.rs │ │ │ ├── relative_selector.rs │ │ │ ├── restyle_hints.rs │ │ │ └── state_and_attributes.rs │ │ ├── media_queries.rs │ │ ├── mod.rs │ │ ├── stylesheets.rs │ │ └── viewport_units.rs │ ├── lib.rs │ ├── logical_geometry.rs │ ├── macros.rs │ ├── matching.rs │ ├── media_queries/ │ │ ├── media_list.rs │ │ ├── media_query.rs │ │ └── mod.rs │ ├── parallel.rs │ ├── parser.rs │ ├── piecewise_linear.rs │ ├── properties/ │ │ ├── build.py │ │ ├── cascade.rs │ │ ├── computed_value_flags.rs │ │ ├── counted_unknown_properties.py │ │ ├── counter_style_descriptors.toml │ │ ├── data.py │ │ ├── declaration_block.rs │ │ ├── font_face_descriptors.toml │ │ ├── gecko.mako.rs │ │ ├── helpers/ │ │ │ └── animated_properties.mako.rs │ │ ├── helpers.mako.rs │ │ ├── longhands.toml │ │ ├── mod.rs │ │ ├── properties.html.mako │ │ ├── properties.mako.rs │ │ ├── property_descriptors.toml │ │ ├── shorthands.rs │ │ ├── shorthands.toml │ │ ├── vendored_python/ │ │ │ ├── mako-1.3.10-py3-none-any.whl │ │ │ ├── markupsafe/ │ │ │ │ ├── LICENSE.txt │ │ │ │ └── __init__.py │ │ │ └── toml-0.10.2-py2.py3-none-any.whl │ │ └── view_transition_descriptors.toml │ ├── properties_and_values/ │ │ ├── mod.rs │ │ ├── registry.rs │ │ ├── rule.rs │ │ ├── syntax/ │ │ │ ├── ascii.rs │ │ │ ├── data_type.rs │ │ │ └── mod.rs │ │ └── value.rs │ ├── queries/ │ │ ├── condition.rs │ │ ├── feature.rs │ │ ├── feature_expression.rs │ │ ├── mod.rs │ │ └── values.rs │ ├── rule_cache.rs │ ├── rule_collector.rs │ ├── rule_tree/ │ │ ├── core.rs │ │ ├── level.rs │ │ ├── map.rs │ │ ├── mod.rs │ │ ├── source.rs │ │ └── unsafe_box.rs │ ├── scoped_tls.rs │ ├── selector_map.rs │ ├── selector_parser.rs │ ├── servo/ │ │ ├── animation.rs │ │ ├── attr.rs │ │ ├── encoding_support.rs │ │ ├── media_features.rs │ │ ├── mod.rs │ │ ├── restyle_damage.rs │ │ ├── selector_parser.rs │ │ ├── shadow_parts.rs │ │ └── url.rs │ ├── shared_lock.rs │ ├── sharing/ │ │ ├── checks.rs │ │ └── mod.rs │ ├── simple_buckets_map.rs │ ├── str.rs │ ├── style_adjuster.rs │ ├── style_resolver.rs │ ├── stylesheet_set.rs │ ├── stylesheets/ │ │ ├── appearance_base_rule.rs │ │ ├── container_rule.rs │ │ ├── counter_style_rule.rs │ │ ├── document_rule.rs │ │ ├── font_face_rule.rs │ │ ├── font_feature_values_rule.rs │ │ ├── font_palette_values_rule.rs │ │ ├── import_rule.rs │ │ ├── keyframes_rule.rs │ │ ├── layer_rule.rs │ │ ├── loader.rs │ │ ├── margin_rule.rs │ │ ├── media_rule.rs │ │ ├── mod.rs │ │ ├── namespace_rule.rs │ │ ├── nested_declarations_rule.rs │ │ ├── origin.rs │ │ ├── page_rule.rs │ │ ├── position_try_rule.rs │ │ ├── property_rule.rs │ │ ├── rule_list.rs │ │ ├── rule_parser.rs │ │ ├── rules_iterator.rs │ │ ├── scope_rule.rs │ │ ├── starting_style_rule.rs │ │ ├── style_rule.rs │ │ ├── stylesheet.rs │ │ ├── supports_rule.rs │ │ └── view_transition_rule.rs │ ├── stylist.rs │ ├── thread_state.rs │ ├── traversal.rs │ ├── traversal_flags.rs │ ├── typed_om/ │ │ ├── mod.rs │ │ ├── numeric_declaration.rs │ │ ├── numeric_values.rs │ │ └── sum_value.rs │ ├── use_counters/ │ │ └── mod.rs │ └── values/ │ ├── animated/ │ │ ├── color.rs │ │ ├── effects.rs │ │ ├── font.rs │ │ ├── grid.rs │ │ ├── lists.rs │ │ ├── mod.rs │ │ ├── svg.rs │ │ └── transform.rs │ ├── computed/ │ │ ├── align.rs │ │ ├── angle.rs │ │ ├── animation.rs │ │ ├── background.rs │ │ ├── basic_shape.rs │ │ ├── border.rs │ │ ├── box.rs │ │ ├── color.rs │ │ ├── column.rs │ │ ├── counters.rs │ │ ├── easing.rs │ │ ├── effects.rs │ │ ├── flex.rs │ │ ├── font.rs │ │ ├── image.rs │ │ ├── length.rs │ │ ├── length_percentage.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ ├── motion.rs │ │ ├── outline.rs │ │ ├── page.rs │ │ ├── percentage.rs │ │ ├── position.rs │ │ ├── ratio.rs │ │ ├── rect.rs │ │ ├── resolution.rs │ │ ├── svg.rs │ │ ├── table.rs │ │ ├── text.rs │ │ ├── time.rs │ │ ├── transform.rs │ │ ├── ui.rs │ │ └── url.rs │ ├── distance.rs │ ├── generics/ │ │ ├── animation.rs │ │ ├── background.rs │ │ ├── basic_shape.rs │ │ ├── border.rs │ │ ├── box.rs │ │ ├── calc.rs │ │ ├── color.rs │ │ ├── column.rs │ │ ├── counters.rs │ │ ├── easing.rs │ │ ├── effects.rs │ │ ├── flex.rs │ │ ├── font.rs │ │ ├── grid.rs │ │ ├── image.rs │ │ ├── length.rs │ │ ├── mod.rs │ │ ├── motion.rs │ │ ├── page.rs │ │ ├── position.rs │ │ ├── ratio.rs │ │ ├── rect.rs │ │ ├── size.rs │ │ ├── svg.rs │ │ ├── text.rs │ │ ├── transform.rs │ │ ├── ui.rs │ │ └── url.rs │ ├── mod.rs │ ├── resolved/ │ │ ├── animation.rs │ │ ├── color.rs │ │ ├── counters.rs │ │ └── mod.rs │ └── specified/ │ ├── align.rs │ ├── angle.rs │ ├── animation.rs │ ├── background.rs │ ├── basic_shape.rs │ ├── border.rs │ ├── box.rs │ ├── calc.rs │ ├── color.rs │ ├── column.rs │ ├── counters.rs │ ├── easing.rs │ ├── effects.rs │ ├── flex.rs │ ├── font.rs │ ├── grid.rs │ ├── image.rs │ ├── intersection_observer.rs │ ├── length.rs │ ├── list.rs │ ├── mod.rs │ ├── motion.rs │ ├── outline.rs │ ├── page.rs │ ├── percentage.rs │ ├── position.rs │ ├── ratio.rs │ ├── rect.rs │ ├── resolution.rs │ ├── source_size_list.rs │ ├── svg.rs │ ├── svg_path.rs │ ├── table.rs │ ├── text.rs │ ├── time.rs │ ├── transform.rs │ ├── ui.rs │ └── url.rs ├── style.paths ├── style_derive/ │ ├── Cargo.toml │ ├── animate.rs │ ├── cg.rs │ ├── compute_squared_distance.rs │ ├── lib.rs │ ├── parse.rs │ ├── specified_value_info.rs │ ├── to_animated_value.rs │ ├── to_animated_zero.rs │ ├── to_computed_value.rs │ ├── to_css.rs │ ├── to_resolved_value.rs │ └── to_typed.rs ├── style_traits/ │ ├── Cargo.toml │ ├── arc_slice.rs │ ├── dom.rs │ ├── lib.rs │ ├── owned_slice.rs │ ├── owned_str.rs │ ├── specified_value_info.rs │ └── values.rs ├── stylo_atoms/ │ ├── Cargo.toml │ ├── build.rs │ ├── lib.rs │ ├── predefined_counter_styles.rs │ └── static_atoms.txt ├── stylo_dom/ │ ├── Cargo.toml │ └── lib.rs ├── stylo_static_prefs/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── sync.sh ├── to_shmem/ │ ├── Cargo.toml │ └── lib.rs └── to_shmem_derive/ ├── Cargo.toml ├── lib.rs ├── to_shmem.rs └── util.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: push: branches: ["main"] pull_request: workflow_dispatch: merge_group: types: [checks_requested] jobs: linux-debug: name: Linux (Debug) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests run: cargo build --features servo env: RUST_BACKTRACE: 1 linux-release: name: Linux (Release) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests run: cargo build --release --features servo env: RUST_BACKTRACE: 1 macos-debug: name: macOS (Debug) runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests run: cargo build --features servo env: RUST_BACKTRACE: 1 windows-debug: name: Windows (Debug) runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Run Tests run: cargo build --features servo env: RUST_BACKTRACE: 1 build-result: name: Result runs-on: ubuntu-latest if: ${{ always() }} needs: - linux-debug - linux-release - macos-debug - windows-debug steps: - name: Success if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} run: exit 0 - name: Failure if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} run: exit 1 ================================================ FILE: .github/workflows/mirror-to-release-branch.yml ================================================ name: 🪞 Mirror `main` on: push: branches: - main jobs: mirror: name: Mirror runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get branch name id: branch-name run: | first_commit=$(git log --pretty=\%H --grep='Servo initial downstream commit') upstream_base="$first_commit~" echo BRANCH_NAME=$(git log -n1 --pretty='%as' $upstream_base) >> $GITHUB_OUTPUT - uses: google/mirror-branch-action@v1.0 name: Mirror to ${{ steps.branch-name.outputs.BRANCH_NAME }} with: github-token: ${{ secrets.GITHUB_TOKEN }} source: main dest: ${{ steps.branch-name.outputs.BRANCH_NAME }} ================================================ FILE: .github/workflows/sync-upstream.yml ================================================ name: Sync upstream with mozilla-central on: schedule: - cron: '0 13 * * *' workflow_dispatch: jobs: sync: name: Sync runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 with: fetch-depth: 1 - uses: actions/cache@v3 with: path: _cache/upstream key: upstream - run: | ./sync.sh _filtered git fetch -f --progress ./_filtered main:upstream git push -fu --progress origin upstream ================================================ FILE: .gitignore ================================================ /_cache/ /_filtered/ /target/ /style/properties/__pycache__/ Cargo.lock ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "stylo_atoms", "stylo_dom", "malloc_size_of", "rustfmt.toml", "selectors", "servo_arc", "style", "style_derive", "stylo_static_prefs", "style_traits", "to_shmem", "to_shmem_derive", ] default-members = ["style"] [workspace.package] version = "0.17.0" [workspace.dependencies] # in-repo dependencies (separately versioned) servo_arc = { version = "0.4.3", path = "./servo_arc" } selectors = { version = "0.38.0", path = "./selectors" } to_shmem = { version = "0.4.0", path = "./to_shmem", features = ["servo"] } to_shmem_derive = { version = "0.1.0", path = "./to_shmem_derive" } # in-repo dependencies (main version) malloc_size_of = { version = "0.17.0", path = "./malloc_size_of", package = "stylo_malloc_size_of", features = ["servo"] } static_prefs = { version = "0.17.0", path = "./stylo_static_prefs", package = "stylo_static_prefs" } stylo_atoms = { version = "0.17.0", path = "./stylo_atoms" } dom = { version = "0.17.0", path = "./stylo_dom", package = "stylo_dom" } style_traits = { version = "0.17.0", path = "./style_traits", features = ["servo"], package = "stylo_traits"} style_derive = { version = "0.17.0", path = "./style_derive", package = "stylo_derive"} stylo = { version = "0.17.0", path = "./style" } ================================================ FILE: README.md ================================================ Stylo ===== **High-Performance CSS Style Engine** [![Build Status](https://github.com/servo/stylo/actions/workflows/main.yml/badge.svg)](https://github.com/servo/stylo/actions) [![Crates.io](https://img.shields.io/crates/v/stylo.svg)](https://crates.io/crates/stylo) [![Docs](https://docs.rs/stylo/badge.svg)](https://docs.rs/stylo) ![Crates.io License](https://img.shields.io/crates/l/stylo) Stylo is a high-performance, browser-grade CSS style engine written in Rust that powers [Servo](https://servo.org) and [Firefox](https://firefox.com). This repo contains Servo’s downstream version of Stylo. The upstream version lives in mozilla-central with the rest of the Gecko/Firefox codebase. Coordination of Stylo development happens: - Here in Github Issues - In the [#stylo](https://servo.zulipchat.com/#narrow/channel/417109-stylo) channel of the [Servo Zulip](https://servo.zulipchat.com/) - In the [#layout](https://chat.mozilla.org/#/room/#layout:mozilla.org) room of the Mozilla Matrix instance (matrix.mozilla.org) ## High-Level Documentation - This [Mozilla Hacks article](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo) contains a high-level overview of the Stylo architecture. - There is a [chapter](https://book.servo.org/architecture/style.html) in the Servo Book (although it is a little out of date). ## Branches The branches are as follows: - [**upstream**](https://github.com/servo/style/tree/upstream) has upstream [mozilla-central](https://searchfox.org/mozilla-central/source/servo) filtered to the paths we care about ([style.paths](style.paths)), but is otherwise unmodified. - [**main**](https://github.com/servo/style/tree/ci) adds our downstream patches, plus the scripts and workflows for syncing with mozilla-central on top of **upstream**. > [!WARNING] > This repo syncs from upstream by creating a new branch and then rebasing our changes on top of it. This means that `git pull` will not work across syncs (you will need to use `git fetch`, `git reset` and `git rebase`). More information on the syncing process is available in [SYNCING.md](SYNCING.md) ## Crates A guide to the crates contained within this repo ### Stylo Crates These crates are largely implementation details of Stylo, although you may need to use some of them directly if you use Stylo. | Directory | Crate | Notes | | --- | --- | --- | | style | [![Crates.io](https://img.shields.io/crates/v/stylo.svg)](https://crates.io/crates/stylo) | The main Stylo crate containing the entire CSS engine | | style_traits | [![Crates.io](https://img.shields.io/crates/v/stylo_traits.svg)](https://crates.io/crates/stylo_traits) | Types and traits which allow other code to interoperate with Stylo without depending on the main crate directly. | | stylo_dom | [![Crates.io](https://img.shields.io/crates/v/stylo_dom.svg)](https://crates.io/crates/stylo_dom) | Similar to stylo_traits (but much smaller) | | stylo_atoms | [![Crates.io](https://img.shields.io/crates/v/stylo_atoms.svg)](https://crates.io/crates/stylo_atoms) | [Atoms](https://docs.rs/string_cache/latest/string_cache/struct.Atom.html) for CSS and HTML event related strings | | stylo_static_prefs | [![Crates.io](https://img.shields.io/crates/v/stylo_static_prefs.svg)](https://crates.io/crates/stylo_static_prefs) | Configuration for Stylo. Can be used to set runtime preferences (enabling/disabling properties, etc) | | style_derive | [![Crates.io](https://img.shields.io/crates/v/stylo_derive.svg)](https://crates.io/crates/stylo_derive) | Internal derive macro for stylo crate | ### Standalone Crates These crates form part of Stylo but are also be useful standalone. | Directory | Crate | Notes | | --- | --- | --- | | selectors | [![Crates.io](https://img.shields.io/crates/v/selectors.svg)](https://crates.io/crates/selectors) | CSS Selector matching | | servo_arc | [![Crates.io](https://img.shields.io/crates/v/servo_arc.svg)](https://crates.io/crates/servo_arc) | A variant on `std::Arc` | You may also be interested in the `cssparser` crate which lives in the [servo/rust-cssparser](https://github.com/servo/rust-cssparser) repo. ### Support Crates Low-level crates which could technically be used standalone but are unlikely to be generally useful in practice. | Directory | Crate | Notes | | --- | --- | --- | | malloc_size_of | [![Crates.io](https://img.shields.io/crates/v/stylo_malloc_size_of.svg)](https://crates.io/crates/stylo_malloc_size_of) | Heap size measurement for Stylo values | | to_shmem | [![Crates.io](https://img.shields.io/crates/v/to_shmem.svg)](https://crates.io/crates/to_shmem) | Internal utility crate for sharing memory across processes. | | to_shmem_derive | [![Crates.io](https://img.shields.io/crates/v/to_shmem_derive.svg)](https://crates.io/crates/to_shmem_derive) | Internal derive macro for to_shmem crate | ## Building Servo Against a Local Copy of Stylo Assuming your local `servo` and `stylo` directories are siblings, you can build `servo` against `stylo` by adding the following to `servo/Cargo.toml`: ```toml [patch."https://github.com/servo/stylo"] selectors = { path = "../stylo/selectors" } servo_arc = { path = "../stylo/servo_arc" } stylo = { path = "../stylo/style" } stylo_atoms = { path = "../stylo/stylo_atoms" } stylo_dom = { path = "../stylo/stylo_dom" } stylo_malloc_size_of = { path = "../stylo/malloc_size_of" } stylo_static_prefs = { path = "../stylo/stylo_static_prefs" } stylo_traits = { path = "../stylo/style_traits" } ``` ## Releases Releases are made every time this repository rebases its changes on top of the latest version of upstream Stylo. There are a lot of crates here. In order to publish them, they must be done in order. One order that works is: - selectors - stylo_static_prefs - stylo_atoms - stylo_malloc_size_of - stylo_dom - stylo_derive - stylo_traits - stylo ## License Stylo is licensed under MPL 2.0 ================================================ FILE: SYNCING.md ================================================ # Syncing This file documents the process of syncing this repository with the upstream copy of Stylo in mozilla-central. ## Syncing `upstream` with mozilla-central Start by generating a filtered copy of mozilla-central. This will cache the raw mozilla-central in `_cache/upstream`, storing the result in `_filtered`: ```sh $ ./sync.sh _filtered ``` If `_filtered` already exists, you will need to delete it and try again: ```sh $ rm -Rf _filtered ``` Now overwrite our `upstream` with those commits and push: ```sh $ git fetch -f --progress ./_filtered main:upstream $ git push -fu --progress origin upstream ``` ## Rebasing `main` onto `upstream` Start by fetching `upstream` into your local repo: ```sh $ git fetch -f origin upstream:upstream ``` In general, the filtering process is deterministic, yielding the same commit hashes each time, so we can rebase normally: ```sh $ git rebase upstream ``` But if the filtering config changes or Mozilla moves to GitHub, the commit hashes on `upstream` may change. In this case, we need to tell git where the old upstream ends and our own commits start (notice the `~`): ```sh $ git log --pretty=\%H --grep='Servo initial downstream commit' e62d7f0090941496e392e1dc91df103a38e3f488 $ git rebase --onto upstream e62d7f0090941496e392e1dc91df103a38e3f488~ Successfully rebased and updated refs/heads/main. ``` `start-rebase.sh` takes care of this automatically, but you should still use `git rebase` for subsequent steps like `--continue` and `--abort`: ```sh $ ./start-rebase.sh upstream $ ./start-rebase.sh upstream -i # interactive $ git rebase --continue # not ./start-rebase.sh --continue $ git rebase --abort # not ./start-rebase.sh --abort ``` Or if we aren’t ready to rebase onto the tip of upstream: ```sh $ ./start-rebase.sh upstream~10 -i ``` ================================================ FILE: commit-from-merge.sh ================================================ #!/bin/sh # Usage: commit-from-merge.sh [extra git-commit(1) arguments ...] # Given a merge commit made by bors, runs git-commit(1) with your local changes # while borrowing the author name/email from the right-hand parent of the merge, # and the author date from the committer date of the merge. set -eu lookup_repo=$1; shift merge_commit=$1; shift author_name_email=$(git -C "$lookup_repo" log -n1 --pretty='%aN <%aE>' "$merge_commit"\^2) committer_date=$(git -C "$lookup_repo" log -n1 --pretty='%cd' "$merge_commit") set -- git commit --author="$author_name_email" --date="$committer_date" "$@" echo "$@" "$@" ================================================ FILE: commit-from-squashed.sh ================================================ #!/bin/sh # Usage: commit-from-squashed.sh [extra git-commit(1) arguments ...] # Given a squashed commit made by the GitHub merge queue, runs git-commit(1) with your local changes # while borrowing our author name/email from that commit, our author date from its committer date, # and our commit message from that commit. set -eu squashed_commit=$1; shift committer_date=$(git log -n1 --pretty='%cd' "$squashed_commit") # -c is equivalent to --author=$(...'%aN <%aE>') -m $(...'%B'), but allows editing set -- git commit -c "$squashed_commit" --date="$committer_date" "$@" echo "$@" "$@" ================================================ FILE: malloc_size_of/Cargo.toml ================================================ [package] name = "stylo_malloc_size_of" version.workspace = true authors = ["The Servo Project Developers"] license = "MIT OR Apache-2.0" repository = "https://github.com/servo/stylo" description = "An allocator-agnostic crate for measuring the heap size of a value" edition = "2021" [lib] path = "lib.rs" [features] gecko = ["thin-vec/gecko-ffi"] servo = ["string_cache"] [dependencies] app_units = "0.7" cssparser = "0.37" euclid = "0.22" selectors = { workspace = true } servo_arc = { workspace = true } smallbitvec = "2.3.0" smallvec = "1.13" string_cache = { version = "0.9", optional = true } thin-vec = { version = "0.2.13" } void = "1.0.2" ================================================ FILE: malloc_size_of/LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: malloc_size_of/LICENSE-MIT ================================================ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: malloc_size_of/lib.rs ================================================ // Copyright 2016-2017 The Servo Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! A crate for measuring the heap usage of data structures in a way that //! integrates with Firefox's memory reporting, particularly the use of //! mozjemalloc and DMD. In particular, it has the following features. //! - It isn't bound to a particular heap allocator. //! - It provides traits for both "shallow" and "deep" measurement, which gives //! flexibility in the cases where the traits can't be used. //! - It allows for measuring blocks even when only an interior pointer can be //! obtained for heap allocations, e.g. `HashSet` and `HashMap`. (This relies //! on the heap allocator having suitable support, which mozjemalloc has.) //! - It allows handling of types like `Rc` and `Arc` by providing traits that //! are different to the ones for non-graph structures. //! //! Suggested uses are as follows. //! - When possible, use the `MallocSizeOf` trait. (Deriving support is //! provided by the `malloc_size_of_derive` crate.) //! - If you need an additional synchronization argument, provide a function //! that is like the standard trait method, but with the extra argument. //! - If you need multiple measurements for a type, provide a function named //! `add_size_of` that takes a mutable reference to a struct that contains //! the multiple measurement fields. //! - When deep measurement (via `MallocSizeOf`) cannot be implemented for a //! type, shallow measurement (via `MallocShallowSizeOf`) in combination with //! iteration can be a useful substitute. //! - `Rc` and `Arc` are always tricky, which is why `MallocSizeOf` is not (and //! should not be) implemented for them. //! - If an `Rc` or `Arc` is known to be a "primary" reference and can always //! be measured, it should be measured via the `MallocUnconditionalSizeOf` //! trait. //! - If an `Rc` or `Arc` should be measured only if it hasn't been seen //! before, it should be measured via the `MallocConditionalSizeOf` trait. //! - Using universal function call syntax is a good idea when measuring boxed //! fields in structs, because it makes it clear that the Box is being //! measured as well as the thing it points to. E.g. //! ` as MallocSizeOf>::size_of(field, ops)`. //! //! Note: WebRender has a reduced fork of this crate, so that we can avoid //! publishing this crate on crates.io. use std::hash::{BuildHasher, Hash}; use std::mem::size_of; use std::ops::Range; use std::ops::{Deref, DerefMut}; use std::os::raw::c_void; use void::Void; /// A C function that takes a pointer to a heap allocation and returns its size. type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize; /// A closure implementing a stateful predicate on pointers. type VoidPtrToBoolFnMut = dyn FnMut(*const c_void) -> bool; /// Operations used when measuring heap usage of data structures. pub struct MallocSizeOfOps { /// A function that returns the size of a heap allocation. size_of_op: VoidPtrToSizeFn, /// Like `size_of_op`, but can take an interior pointer. Optional because /// not all allocators support this operation. If it's not provided, some /// memory measurements will actually be computed estimates rather than /// real and accurate measurements. enclosing_size_of_op: Option, /// Check if a pointer has been seen before, and remember it for next time. /// Useful when measuring `Rc`s and `Arc`s. Optional, because many places /// don't need it. have_seen_ptr_op: Option>, } impl MallocSizeOfOps { pub fn new( size_of: VoidPtrToSizeFn, malloc_enclosing_size_of: Option, have_seen_ptr: Option>, ) -> Self { MallocSizeOfOps { size_of_op: size_of, enclosing_size_of_op: malloc_enclosing_size_of, have_seen_ptr_op: have_seen_ptr, } } /// Check if an allocation is empty. This relies on knowledge of how Rust /// handles empty allocations, which may change in the future. fn is_empty(ptr: *const T) -> bool { // The correct condition is this: // `ptr as usize <= ::std::mem::align_of::()` // But we can't call align_of() on a ?Sized T. So we approximate it // with the following. 256 is large enough that it should always be // larger than the required alignment, but small enough that it is // always in the first page of memory and therefore not a legitimate // address. return ptr as *const usize as usize <= 256; } /// Call `size_of_op` on `ptr`, first checking that the allocation isn't /// empty, because some types (such as `Vec`) utilize empty allocations. pub unsafe fn malloc_size_of(&self, ptr: *const T) -> usize { if MallocSizeOfOps::is_empty(ptr) { 0 } else { (self.size_of_op)(ptr as *const c_void) } } /// Is an `enclosing_size_of_op` available? pub fn has_malloc_enclosing_size_of(&self) -> bool { self.enclosing_size_of_op.is_some() } /// Call `enclosing_size_of_op`, which must be available, on `ptr`, which /// must not be empty. pub unsafe fn malloc_enclosing_size_of(&self, ptr: *const T) -> usize { assert!(!MallocSizeOfOps::is_empty(ptr)); (self.enclosing_size_of_op.unwrap())(ptr as *const c_void) } /// Call `have_seen_ptr_op` on `ptr`. pub fn have_seen_ptr(&mut self, ptr: *const T) -> bool { let have_seen_ptr_op = self .have_seen_ptr_op .as_mut() .expect("missing have_seen_ptr_op"); have_seen_ptr_op(ptr as *const c_void) } } /// Trait for measuring the "deep" heap usage of a data structure. This is the /// most commonly-used of the traits. pub trait MallocSizeOf { /// Measure the heap usage of all descendant heap-allocated structures, but /// not the space taken up by the value itself. fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } /// Trait for measuring the "shallow" heap usage of a container. pub trait MallocShallowSizeOf { /// Measure the heap usage of immediate heap-allocated descendant /// structures, but not the space taken up by the value itself. Anything /// beyond the immediate descendants must be measured separately, using /// iteration. fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } /// Like `MallocSizeOf`, but with a different name so it cannot be used /// accidentally with derive(MallocSizeOf). For use with types like `Rc` and /// `Arc` when appropriate (e.g. when measuring a "primary" reference). pub trait MallocUnconditionalSizeOf { /// Measure the heap usage of all heap-allocated descendant structures, but /// not the space taken up by the value itself. fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } /// `MallocUnconditionalSizeOf` combined with `MallocShallowSizeOf`. pub trait MallocUnconditionalShallowSizeOf { /// `unconditional_size_of` combined with `shallow_size_of`. fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } /// Like `MallocSizeOf`, but only measures if the value hasn't already been /// measured. For use with types like `Rc` and `Arc` when appropriate (e.g. /// when there is no "primary" reference). pub trait MallocConditionalSizeOf { /// Measure the heap usage of all heap-allocated descendant structures, but /// not the space taken up by the value itself, and only if that heap usage /// hasn't already been measured. fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } /// `MallocConditionalSizeOf` combined with `MallocShallowSizeOf`. pub trait MallocConditionalShallowSizeOf { /// `conditional_size_of` combined with `shallow_size_of`. fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; } impl MallocSizeOf for String { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { unsafe { ops.malloc_size_of(self.as_ptr()) } } } impl<'a, T: ?Sized> MallocSizeOf for &'a T { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { // Zero makes sense for a non-owning reference. 0 } } impl MallocShallowSizeOf for Box { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { unsafe { ops.malloc_size_of(&**self) } } } impl MallocSizeOf for Box { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.shallow_size_of(ops) + (**self).size_of(ops) } } impl MallocSizeOf for () { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } } impl MallocSizeOf for (T1, T2) where T1: MallocSizeOf, T2: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.0.size_of(ops) + self.1.size_of(ops) } } impl MallocSizeOf for (T1, T2, T3) where T1: MallocSizeOf, T2: MallocSizeOf, T3: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) } } impl MallocSizeOf for (T1, T2, T3, T4) where T1: MallocSizeOf, T2: MallocSizeOf, T3: MallocSizeOf, T4: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + self.3.size_of(ops) } } impl MallocSizeOf for Option { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if let Some(val) = self.as_ref() { val.size_of(ops) } else { 0 } } } impl MallocSizeOf for Result { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { match *self { Ok(ref x) => x.size_of(ops), Err(ref e) => e.size_of(ops), } } } impl MallocSizeOf for std::cell::Cell { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.get().size_of(ops) } } impl MallocSizeOf for std::cell::RefCell { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.borrow().size_of(ops) } } impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B> where B::Owned: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { match *self { std::borrow::Cow::Borrowed(_) => 0, std::borrow::Cow::Owned(ref b) => b.size_of(ops), } } } impl MallocSizeOf for [T] { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = 0; for elem in self.iter() { n += elem.size_of(ops); } n } } impl MallocShallowSizeOf for Vec { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { unsafe { ops.malloc_size_of(self.as_ptr()) } } } impl MallocSizeOf for Vec { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for elem in self.iter() { n += elem.size_of(ops); } n } } impl MallocShallowSizeOf for std::collections::VecDeque { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if ops.has_malloc_enclosing_size_of() { if let Some(front) = self.front() { // The front element is an interior pointer. unsafe { ops.malloc_enclosing_size_of(&*front) } } else { // This assumes that no memory is allocated when the VecDeque is empty. 0 } } else { // An estimate. self.capacity() * size_of::() } } } impl MallocSizeOf for std::collections::VecDeque { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for elem in self.iter() { n += elem.size_of(ops); } n } } impl MallocShallowSizeOf for smallvec::SmallVec { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if self.spilled() { unsafe { ops.malloc_size_of(self.as_ptr()) } } else { 0 } } } impl MallocSizeOf for smallvec::SmallVec where A: smallvec::Array, A::Item: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for elem in self.iter() { n += elem.size_of(ops); } n } } impl MallocShallowSizeOf for thin_vec::ThinVec { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if self.capacity() == 0 { // If it's the singleton we might not be a heap pointer. return 0; } assert_eq!( std::mem::size_of::(), std::mem::size_of::<*const ()>() ); unsafe { ops.malloc_size_of(*(self as *const Self as *const *const ())) } } } impl MallocSizeOf for thin_vec::ThinVec { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for elem in self.iter() { n += elem.size_of(ops); } n } } macro_rules! malloc_size_of_hash_set { ($ty:ty) => { impl MallocShallowSizeOf for $ty where T: Eq + Hash, S: BuildHasher, { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if ops.has_malloc_enclosing_size_of() { // The first value from the iterator gives us an interior pointer. // `ops.malloc_enclosing_size_of()` then gives us the storage size. // This assumes that the `HashSet`'s contents (values and hashes) // are all stored in a single contiguous heap allocation. self.iter() .next() .map_or(0, |t| unsafe { ops.malloc_enclosing_size_of(t) }) } else { // An estimate. self.capacity() * (size_of::() + size_of::()) } } } impl MallocSizeOf for $ty where T: Eq + Hash + MallocSizeOf, S: BuildHasher, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for t in self.iter() { n += t.size_of(ops); } n } } }; } malloc_size_of_hash_set!(std::collections::HashSet); macro_rules! malloc_size_of_hash_map { ($ty:ty) => { impl MallocShallowSizeOf for $ty where K: Eq + Hash, S: BuildHasher, { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { // See the implementation for std::collections::HashSet for details. if ops.has_malloc_enclosing_size_of() { self.values() .next() .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) } else { self.capacity() * (size_of::() + size_of::() + size_of::()) } } } impl MallocSizeOf for $ty where K: Eq + Hash + MallocSizeOf, V: MallocSizeOf, S: BuildHasher, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for (k, v) in self.iter() { n += k.size_of(ops); n += v.size_of(ops); } n } } }; } malloc_size_of_hash_map!(std::collections::HashMap); impl MallocShallowSizeOf for std::collections::BTreeMap where K: Eq + Hash, { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if ops.has_malloc_enclosing_size_of() { self.values() .next() .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) } else { self.len() * (size_of::() + size_of::() + size_of::()) } } } impl MallocSizeOf for std::collections::BTreeMap where K: Eq + Hash + MallocSizeOf, V: MallocSizeOf, { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = self.shallow_size_of(ops); for (k, v) in self.iter() { n += k.size_of(ops); n += v.size_of(ops); } n } } // PhantomData is always 0. impl MallocSizeOf for std::marker::PhantomData { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } } // XXX: we don't want MallocSizeOf to be defined for Rc and Arc. If negative // trait bounds are ever allowed, this code should be uncommented. // (We do have a compile-fail test for this: // rc_arc_must_not_derive_malloc_size_of.rs) //impl !MallocSizeOf for Arc { } //impl !MallocShallowSizeOf for Arc { } impl MallocUnconditionalShallowSizeOf for servo_arc::Arc { fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { unsafe { ops.malloc_size_of(self.heap_ptr()) } } } impl MallocUnconditionalSizeOf for servo_arc::Arc { fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.unconditional_shallow_size_of(ops) + (**self).size_of(ops) } } impl MallocConditionalShallowSizeOf for servo_arc::Arc { fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if ops.have_seen_ptr(self.heap_ptr()) { 0 } else { self.unconditional_shallow_size_of(ops) } } } impl MallocConditionalSizeOf for servo_arc::Arc { fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if ops.have_seen_ptr(self.heap_ptr()) { 0 } else { self.unconditional_size_of(ops) } } } /// If a mutex is stored directly as a member of a data type that is being measured, /// it is the unique owner of its contents and deserves to be measured. /// /// If a mutex is stored inside of an Arc value as a member of a data type that is being measured, /// the Arc will not be automatically measured so there is no risk of overcounting the mutex's /// contents. impl MallocSizeOf for std::sync::Mutex { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { (*self.lock().unwrap()).size_of(ops) } } impl MallocSizeOf for smallbitvec::SmallBitVec { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { if let Some(ptr) = self.heap_ptr() { unsafe { ops.malloc_size_of(ptr) } } else { 0 } } } impl MallocSizeOf for euclid::Length { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.0.size_of(ops) } } impl MallocSizeOf for euclid::Scale { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.0.size_of(ops) } } impl MallocSizeOf for euclid::Point2D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.x.size_of(ops) + self.y.size_of(ops) } } impl MallocSizeOf for euclid::Rect { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.origin.size_of(ops) + self.size.size_of(ops) } } impl MallocSizeOf for euclid::SideOffsets2D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.top.size_of(ops) + self.right.size_of(ops) + self.bottom.size_of(ops) + self.left.size_of(ops) } } impl MallocSizeOf for euclid::Size2D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.width.size_of(ops) + self.height.size_of(ops) } } impl MallocSizeOf for euclid::Transform2D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.m11.size_of(ops) + self.m12.size_of(ops) + self.m21.size_of(ops) + self.m22.size_of(ops) + self.m31.size_of(ops) + self.m32.size_of(ops) } } impl MallocSizeOf for euclid::Transform3D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.m11.size_of(ops) + self.m12.size_of(ops) + self.m13.size_of(ops) + self.m14.size_of(ops) + self.m21.size_of(ops) + self.m22.size_of(ops) + self.m23.size_of(ops) + self.m24.size_of(ops) + self.m31.size_of(ops) + self.m32.size_of(ops) + self.m33.size_of(ops) + self.m34.size_of(ops) + self.m41.size_of(ops) + self.m42.size_of(ops) + self.m43.size_of(ops) + self.m44.size_of(ops) } } impl MallocSizeOf for euclid::Vector2D { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { self.x.size_of(ops) + self.y.size_of(ops) } } impl MallocSizeOf for selectors::parser::AncestorHashes { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let selectors::parser::AncestorHashes { ref packed_hashes } = *self; packed_hashes.size_of(ops) } } impl MallocUnconditionalSizeOf for selectors::parser::Selector where Impl::NonTSPseudoClass: MallocSizeOf, Impl::PseudoElement: MallocSizeOf, { fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = 0; // It's OK to measure this ThinArc directly because it's the // "primary" reference. (The secondary references are on the // Stylist.) n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; for component in self.iter_raw_match_order() { n += component.size_of(ops); } n } } impl MallocUnconditionalSizeOf for selectors::parser::SelectorList where Impl::NonTSPseudoClass: MallocSizeOf, Impl::PseudoElement: MallocSizeOf, { fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { let mut n = 0; // It's OK to measure this ThinArc directly because it's the "primary" reference. (The // secondary references are on the Stylist.) n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; if self.len() > 1 { for selector in self.slice().iter() { n += selector.size_of(ops); } } n } } impl MallocUnconditionalSizeOf for selectors::parser::Component where Impl::NonTSPseudoClass: MallocSizeOf, Impl::PseudoElement: MallocSizeOf, { fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { use selectors::parser::Component; match self { Component::AttributeOther(ref attr_selector) => attr_selector.size_of(ops), Component::Negation(ref components) => components.unconditional_size_of(ops), Component::NonTSPseudoClass(ref pseudo) => (*pseudo).size_of(ops), Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => { selector.unconditional_size_of(ops) }, Component::Is(ref list) | Component::Where(ref list) => list.unconditional_size_of(ops), Component::Has(ref relative_selectors) => relative_selectors.size_of(ops), Component::NthOf(ref nth_of_data) => nth_of_data.size_of(ops), Component::PseudoElement(ref pseudo) => (*pseudo).size_of(ops), Component::Combinator(..) | Component::ExplicitAnyNamespace | Component::ExplicitNoNamespace | Component::DefaultNamespace(..) | Component::Namespace(..) | Component::ExplicitUniversalType | Component::LocalName(..) | Component::ID(..) | Component::Part(..) | Component::Class(..) | Component::AttributeInNoNamespaceExists { .. } | Component::AttributeInNoNamespace { .. } | Component::Root | Component::Empty | Component::Scope | Component::ImplicitScope | Component::ParentSelector | Component::Nth(..) | Component::Host(None) | Component::RelativeSelectorAnchor | Component::Invalid(..) => 0, } } } impl MallocSizeOf for selectors::attr::AttrSelectorWithOptionalNamespace { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } } impl MallocSizeOf for selectors::parser::AnPlusB { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } } impl MallocSizeOf for Void { #[inline] fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { void::unreachable(*self) } } #[cfg(feature = "servo")] impl MallocSizeOf for string_cache::Atom { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 } } /// For use on types where size_of() returns 0. #[macro_export] macro_rules! malloc_size_of_is_0( ($($ty:ty),+) => ( $( impl $crate::MallocSizeOf for $ty { #[inline(always)] fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { 0 } } )+ ); ($($ty:ident<$($gen:ident),+>),+) => ( $( impl<$($gen: $crate::MallocSizeOf),+> $crate::MallocSizeOf for $ty<$($gen),+> { #[inline(always)] fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { 0 } } )+ ); ); malloc_size_of_is_0!(bool, char, str); malloc_size_of_is_0!(u8, u16, u32, u64, u128, usize); malloc_size_of_is_0!(i8, i16, i32, i64, i128, isize); malloc_size_of_is_0!(f32, f64); malloc_size_of_is_0!(std::sync::atomic::AtomicBool); malloc_size_of_is_0!(std::sync::atomic::AtomicIsize); malloc_size_of_is_0!(std::sync::atomic::AtomicU32); malloc_size_of_is_0!(std::sync::atomic::AtomicUsize); malloc_size_of_is_0!(std::num::NonZeroUsize); malloc_size_of_is_0!(std::num::NonZeroU64); malloc_size_of_is_0!(Range, Range, Range, Range, Range); malloc_size_of_is_0!(Range, Range, Range, Range, Range); malloc_size_of_is_0!(Range, Range); malloc_size_of_is_0!(app_units::Au); malloc_size_of_is_0!( cssparser::TokenSerializationType, cssparser::SourceLocation, cssparser::SourcePosition, cssparser::UnicodeRange ); malloc_size_of_is_0!(selectors::OpaqueElement); /// Measurable that defers to inner value and used to verify MallocSizeOf implementation in a /// struct. #[derive(Clone)] pub struct Measurable(pub T); impl Deref for Measurable { type Target = T; fn deref(&self) -> &T { &self.0 } } impl DerefMut for Measurable { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } ================================================ FILE: rustfmt.toml ================================================ match_block_trailing_comma = true reorder_imports = true ================================================ FILE: selectors/CHANGES.md ================================================ - `parser.rs` no longer wraps values in quotes (`"..."`) but expects their `to_css` impl to already wrap it ([Gecko Bug 1854809](https://bugzilla.mozilla.org/show_bug.cgi?id=1854809)) ================================================ FILE: selectors/Cargo.toml ================================================ [package] name = "selectors" version = "0.38.0" authors = ["The Servo Project Developers"] documentation = "https://docs.rs/selectors/" description = "CSS Selectors matching for Rust" repository = "https://github.com/servo/stylo" readme = "README.md" keywords = ["css", "selectors"] license = "MPL-2.0" edition = "2021" build = "build.rs" [lib] name = "selectors" path = "lib.rs" [features] bench = [] to_shmem = ["dep:to_shmem", "dep:to_shmem_derive"] [dependencies] bitflags = "2" cssparser = "0.37" derive_more = { version = "2", features = ["add", "add_assign"] } rustc-hash = "2.1.1" log = "0.4" phf = "0.13" precomputed-hash = "0.1" servo_arc = { workspace = true } smallvec = "1.0" to_shmem = { workspace = true, optional = true } to_shmem_derive = { workspace = true, optional = true } new_debug_unreachable = "1" [build-dependencies] phf_codegen = "0.13" ================================================ FILE: selectors/README.md ================================================ rust-selectors ============== * [![Build Status](https://github.com/servo/stylo/actions/workflows/main.yml/badge.svg)](https://github.com/servo/stylo/actions) * [Documentation](https://docs.rs/selectors) * [crates.io](https://crates.io/crates/selectors) CSS Selectors library for Rust. Includes parsing and serilization of selectors, as well as matching against a generic tree of elements. Pseudo-elements and most pseudo-classes are generic as well. **Warning:** breaking changes are made to this library fairly frequently (13 times in 2016, for example). However you can use this crate without updating it that often, old versions stay available on crates.io and Cargo will only automatically update to versions that are numbered as compatible. To see how to use this library with your own tree representation, see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs). (Note however that Kuchiki is not always up to date with the latest rust-selectors version, so that code may need to be tweaked.) If you don’t already have a tree data structure, consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself. ================================================ FILE: selectors/attr.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::parser::SelectorImpl; use cssparser::ToCss; use std::fmt; #[cfg(feature = "to_shmem")] use to_shmem_derive::ToShmem; #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct AttrSelectorWithOptionalNamespace { #[cfg_attr(feature = "to_shmem", shmem(field_bound))] pub namespace: Option>, #[cfg_attr(feature = "to_shmem", shmem(field_bound))] pub local_name: Impl::LocalName, pub local_name_lower: Impl::LocalName, #[cfg_attr(feature = "to_shmem", shmem(field_bound))] pub operation: ParsedAttrSelectorOperation, } impl AttrSelectorWithOptionalNamespace { pub fn namespace(&self) -> Option> { self.namespace.as_ref().map(|ns| match ns { NamespaceConstraint::Any => NamespaceConstraint::Any, NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url), }) } } #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum NamespaceConstraint { Any, /// Empty string for no namespace Specific(NamespaceUrl), } #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum ParsedAttrSelectorOperation { Exists, WithValue { operator: AttrSelectorOperator, case_sensitivity: ParsedCaseSensitivity, value: AttrValue, }, } #[derive(Clone, Eq, PartialEq)] pub enum AttrSelectorOperation { Exists, WithValue { operator: AttrSelectorOperator, case_sensitivity: CaseSensitivity, value: AttrValue, }, } impl AttrSelectorOperation { pub fn eval_str(&self, element_attr_value: &str) -> bool where AttrValue: AsRef, { match *self { AttrSelectorOperation::Exists => true, AttrSelectorOperation::WithValue { operator, case_sensitivity, ref value, } => operator.eval_str(element_attr_value, value.as_ref(), case_sensitivity), } } } #[derive(Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum AttrSelectorOperator { Equal, Includes, DashMatch, Prefix, Substring, Suffix, } impl ToCss for AttrSelectorOperator { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { // https://drafts.csswg.org/cssom/#serializing-selectors // See "attribute selector". dest.write_str(match *self { AttrSelectorOperator::Equal => "=", AttrSelectorOperator::Includes => "~=", AttrSelectorOperator::DashMatch => "|=", AttrSelectorOperator::Prefix => "^=", AttrSelectorOperator::Substring => "*=", AttrSelectorOperator::Suffix => "$=", }) } } impl AttrSelectorOperator { pub fn eval_str( self, element_attr_value: &str, attr_selector_value: &str, case_sensitivity: CaseSensitivity, ) -> bool { let e = element_attr_value.as_bytes(); let s = attr_selector_value.as_bytes(); let case = case_sensitivity; match self { AttrSelectorOperator::Equal => case.eq(e, s), AttrSelectorOperator::Prefix => { !s.is_empty() && e.len() >= s.len() && case.eq(&e[..s.len()], s) }, AttrSelectorOperator::Suffix => { !s.is_empty() && e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s) }, AttrSelectorOperator::Substring => { !s.is_empty() && case.contains(element_attr_value, attr_selector_value) }, AttrSelectorOperator::Includes => { !s.is_empty() && element_attr_value .split(SELECTOR_WHITESPACE) .any(|part| case.eq(part.as_bytes(), s)) }, AttrSelectorOperator::DashMatch => { case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s)) }, } } } /// The definition of whitespace per CSS Selectors Level 3 § 4. pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C']; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum ParsedCaseSensitivity { /// 's' was specified. ExplicitCaseSensitive, /// 'i' was specified. AsciiCaseInsensitive, /// No flags were specified and HTML says this is a case-sensitive attribute. CaseSensitive, /// No flags were specified and HTML says this is a case-insensitive attribute. AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CaseSensitivity { CaseSensitive, AsciiCaseInsensitive, } impl CaseSensitivity { pub fn eq(self, a: &[u8], b: &[u8]) -> bool { match self { CaseSensitivity::CaseSensitive => a == b, CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), } } pub fn contains(self, haystack: &str, needle: &str) -> bool { match self { CaseSensitivity::CaseSensitive => haystack.contains(needle), CaseSensitivity::AsciiCaseInsensitive => { if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() { haystack.bytes().enumerate().any(|(i, byte)| { if !byte.eq_ignore_ascii_case(&n_first_byte) { return false; } let after_this_byte = &haystack.as_bytes()[i + 1..]; match after_this_byte.get(..n_rest.len()) { None => false, Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest), } }) } else { // any_str.contains("") == true, // though these cases should be handled with *NeverMatches and never go here. true } }, } } } ================================================ FILE: selectors/bloom.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Counting and non-counting Bloom filters tuned for use as ancestor filters //! for selector matching. use std::fmt::{self, Debug}; // The top 8 bits of the 32-bit hash value are not used by the bloom filter. // Consumers may rely on this to pack hashes more efficiently. pub const BLOOM_HASH_MASK: u32 = 0x00ffffff; const KEY_SIZE: usize = 12; const ARRAY_SIZE: usize = 1 << KEY_SIZE; const KEY_MASK: u32 = (1 << KEY_SIZE) - 1; /// A counting Bloom filter with 8-bit counters. pub type BloomFilter = CountingBloomFilter; /// A counting Bloom filter with parameterized storage to handle /// counters of different sizes. For now we assume that having two hash /// functions is enough, but we may revisit that decision later. /// /// The filter uses an array with 2**KeySize entries. /// /// Assuming a well-distributed hash function, a Bloom filter with /// array size M containing N elements and /// using k hash function has expected false positive rate exactly /// /// $ (1 - (1 - 1/M)^{kN})^k $ /// /// because each array slot has a /// /// $ (1 - 1/M)^{kN} $ /// /// chance of being 0, and the expected false positive rate is the /// probability that all of the k hash functions will hit a nonzero /// slot. /// /// For reasonable assumptions (M large, kN large, which should both /// hold if we're worried about false positives) about M and kN this /// becomes approximately /// /// $$ (1 - \exp(-kN/M))^k $$ /// /// For our special case of k == 2, that's $(1 - \exp(-2N/M))^2$, /// or in other words /// /// $$ N/M = -0.5 * \ln(1 - \sqrt(r)) $$ /// /// where r is the false positive rate. This can be used to compute /// the desired KeySize for a given load N and false positive rate r. /// /// If N/M is assumed small, then the false positive rate can /// further be approximated as 4*N^2/M^2. So increasing KeySize by /// 1, which doubles M, reduces the false positive rate by about a /// factor of 4, and a false positive rate of 1% corresponds to /// about M/N == 20. /// /// What this means in practice is that for a few hundred keys using a /// KeySize of 12 gives false positive rates on the order of 0.25-4%. /// /// Similarly, using a KeySize of 10 would lead to a 4% false /// positive rate for N == 100 and to quite bad false positive /// rates for larger N. #[derive(Clone, Default)] pub struct CountingBloomFilter where S: BloomStorage, { storage: S, } impl CountingBloomFilter where S: BloomStorage, { /// Creates a new bloom filter. #[inline] pub fn new() -> Self { Default::default() } #[inline] pub fn clear(&mut self) { self.storage = Default::default(); } // Slow linear accessor to make sure the bloom filter is zeroed. This should // never be used in release builds. #[cfg(debug_assertions)] pub fn is_zeroed(&self) -> bool { self.storage.is_zeroed() } #[cfg(not(debug_assertions))] pub fn is_zeroed(&self) -> bool { unreachable!() } /// Inserts an item with a particular hash into the bloom filter. #[inline] pub fn insert_hash(&mut self, hash: u32) { self.storage.adjust_first_slot(hash, true); self.storage.adjust_second_slot(hash, true); } /// Removes an item with a particular hash from the bloom filter. #[inline] pub fn remove_hash(&mut self, hash: u32) { self.storage.adjust_first_slot(hash, false); self.storage.adjust_second_slot(hash, false); } /// Check whether the filter might contain an item with the given hash. /// This can sometimes return true even if the item is not in the filter, /// but will never return false for items that are actually in the filter. #[inline] pub fn might_contain_hash(&self, hash: u32) -> bool { !self.storage.first_slot_is_empty(hash) && !self.storage.second_slot_is_empty(hash) } } impl Debug for CountingBloomFilter where S: BloomStorage, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut slots_used = 0; for i in 0..ARRAY_SIZE { if !self.storage.slot_is_empty(i) { slots_used += 1; } } write!(f, "BloomFilter({}/{})", slots_used, ARRAY_SIZE) } } pub trait BloomStorage: Clone + Default { fn slot_is_empty(&self, index: usize) -> bool; fn adjust_slot(&mut self, index: usize, increment: bool); fn is_zeroed(&self) -> bool; #[inline] fn first_slot_is_empty(&self, hash: u32) -> bool { self.slot_is_empty(Self::first_slot_index(hash)) } #[inline] fn second_slot_is_empty(&self, hash: u32) -> bool { self.slot_is_empty(Self::second_slot_index(hash)) } #[inline] fn adjust_first_slot(&mut self, hash: u32, increment: bool) { self.adjust_slot(Self::first_slot_index(hash), increment) } #[inline] fn adjust_second_slot(&mut self, hash: u32, increment: bool) { self.adjust_slot(Self::second_slot_index(hash), increment) } #[inline] fn first_slot_index(hash: u32) -> usize { hash1(hash) as usize } #[inline] fn second_slot_index(hash: u32) -> usize { hash2(hash) as usize } } /// Storage class for a CountingBloomFilter that has 8-bit counters. pub struct BloomStorageU8 { counters: [u8; ARRAY_SIZE], } impl BloomStorage for BloomStorageU8 { #[inline] fn adjust_slot(&mut self, index: usize, increment: bool) { let slot = &mut self.counters[index]; if *slot != 0xff { // full if increment { *slot += 1; } else { *slot -= 1; } } } #[inline] fn slot_is_empty(&self, index: usize) -> bool { self.counters[index] == 0 } #[inline] fn is_zeroed(&self) -> bool { self.counters.iter().all(|x| *x == 0) } } impl Default for BloomStorageU8 { fn default() -> Self { BloomStorageU8 { counters: [0; ARRAY_SIZE], } } } impl Clone for BloomStorageU8 { fn clone(&self) -> Self { BloomStorageU8 { counters: self.counters, } } } /// Storage class for a CountingBloomFilter that has 1-bit counters. pub struct BloomStorageBool { counters: [u8; ARRAY_SIZE / 8], } impl BloomStorage for BloomStorageBool { #[inline] fn adjust_slot(&mut self, index: usize, increment: bool) { let bit = 1 << (index % 8); let byte = &mut self.counters[index / 8]; // Since we have only one bit for storage, decrementing it // should never do anything. Assert against an accidental // decrementing of a bit that was never set. assert!( increment || (*byte & bit) != 0, "should not decrement if slot is already false" ); if increment { *byte |= bit; } } #[inline] fn slot_is_empty(&self, index: usize) -> bool { let bit = 1 << (index % 8); (self.counters[index / 8] & bit) == 0 } #[inline] fn is_zeroed(&self) -> bool { self.counters.iter().all(|x| *x == 0) } } impl Default for BloomStorageBool { fn default() -> Self { BloomStorageBool { counters: [0; ARRAY_SIZE / 8], } } } impl Clone for BloomStorageBool { fn clone(&self) -> Self { BloomStorageBool { counters: self.counters, } } } #[inline] fn hash1(hash: u32) -> u32 { hash & KEY_MASK } #[inline] fn hash2(hash: u32) -> u32 { (hash >> KEY_SIZE) & KEY_MASK } #[test] fn create_and_insert_some_stuff() { use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; use std::mem::transmute; fn hash_as_str(i: usize) -> u32 { let mut hasher = FxHasher::default(); let s = i.to_string(); s.hash(&mut hasher); let hash: u64 = hasher.finish(); (hash >> 32) as u32 ^ (hash as u32) } let mut bf = BloomFilter::new(); // Statically assert that ARRAY_SIZE is a multiple of 8, which // BloomStorageBool relies on. unsafe { transmute::<[u8; ARRAY_SIZE % 8], [u8; 0]>([]); } for i in 0_usize..1000 { bf.insert_hash(hash_as_str(i)); } for i in 0_usize..1000 { assert!(bf.might_contain_hash(hash_as_str(i))); } let false_positives = (1001_usize..2000) .filter(|i| bf.might_contain_hash(hash_as_str(*i))) .count(); assert!(false_positives < 190, "{} is not < 190", false_positives); // 19%. for i in 0_usize..100 { bf.remove_hash(hash_as_str(i)); } for i in 100_usize..1000 { assert!(bf.might_contain_hash(hash_as_str(i))); } let false_positives = (0_usize..100) .filter(|i| bf.might_contain_hash(hash_as_str(*i))) .count(); assert!(false_positives < 20, "{} is not < 20", false_positives); // 20%. bf.clear(); for i in 0_usize..2000 { assert!(!bf.might_contain_hash(hash_as_str(i))); } } #[cfg(feature = "bench")] #[cfg(test)] mod bench { extern crate test; use super::BloomFilter; #[derive(Default)] struct HashGenerator(u32); impl HashGenerator { fn next(&mut self) -> u32 { // Each hash is split into two twelve-bit segments, which are used // as an index into an array. We increment each by 64 so that we // hit the next cache line, and then another 1 so that our wrapping // behavior leads us to different entries each time. // // Trying to simulate cold caches is rather difficult with the cargo // benchmarking setup, so it may all be moot depending on the number // of iterations that end up being run. But we might as well. self.0 += (65) + (65 << super::KEY_SIZE); self.0 } } #[bench] fn create_insert_1000_remove_100_lookup_100(b: &mut test::Bencher) { b.iter(|| { let mut gen1 = HashGenerator::default(); let mut gen2 = HashGenerator::default(); let mut bf = BloomFilter::new(); for _ in 0_usize..1000 { bf.insert_hash(gen1.next()); } for _ in 0_usize..100 { bf.remove_hash(gen2.next()); } for _ in 100_usize..200 { test::black_box(bf.might_contain_hash(gen2.next())); } }); } #[bench] fn might_contain_10(b: &mut test::Bencher) { let bf = BloomFilter::new(); let mut gen = HashGenerator::default(); b.iter(|| { for _ in 0..10 { test::black_box(bf.might_contain_hash(gen.next())); } }); } #[bench] fn clear(b: &mut test::Bencher) { let mut bf = Box::new(BloomFilter::new()); b.iter(|| test::black_box(&mut bf).clear()); } #[bench] fn insert_10(b: &mut test::Bencher) { let mut bf = BloomFilter::new(); let mut gen = HashGenerator::default(); b.iter(|| { for _ in 0..10 { test::black_box(bf.insert_hash(gen.next())); } }); } #[bench] fn remove_10(b: &mut test::Bencher) { let mut bf = BloomFilter::new(); let mut gen = HashGenerator::default(); // Note: this will underflow, and that's ok. b.iter(|| { for _ in 0..10 { bf.remove_hash(gen.next()) } }); } } ================================================ FILE: selectors/build.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ extern crate phf_codegen; use std::env; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; fn main() { let path = Path::new(&env::var_os("OUT_DIR").unwrap()) .join("ascii_case_insensitive_html_attributes.rs"); let mut file = BufWriter::new(File::create(&path).unwrap()); let mut set = phf_codegen::Set::new(); for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { set.entry(name); } write!( &mut file, "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}", set.build(), ) .unwrap(); } /// static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#" accept accept-charset align alink axis bgcolor charset checked clear codetype color compact declare defer dir direction disabled enctype face frame hreflang http-equiv lang language link media method multiple nohref noresize noshade nowrap readonly rel rev rules scope scrolling selected shape target text type valign valuetype vlink "#; ================================================ FILE: selectors/builder.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Helper module to build up a selector safely and efficiently. //! //! Our selector representation is designed to optimize matching, and has //! several requirements: //! * All simple selectors and combinators are stored inline in the same buffer as Component //! instances. //! * We store the top-level compound selectors from right to left, i.e. in matching order. //! * We store the simple selectors for each combinator from left to right, so that we match the //! cheaper simple selectors first. //! //! For example, the selector: //! //! .bar:hover > .baz:nth-child(2) + .qux //! //! Gets stored as: //! //! [.qux, + , .baz, :nth-child(2), > , .bar, :hover] //! //! Meeting all these constraints without extra memmove traffic during parsing is non-trivial. This //! module encapsulates those details and presents an easy-to-use API for the parser. use crate::parser::{ Combinator, Component, ParseRelative, RelativeSelector, Selector, SelectorData, SelectorImpl, }; use crate::sink::Push; use bitflags::bitflags; use derive_more::{Add, AddAssign}; use servo_arc::Arc; use smallvec::SmallVec; use std::cmp; use std::slice; #[cfg(feature = "to_shmem")] use to_shmem_derive::ToShmem; /// Top-level SelectorBuilder struct. This should be stack-allocated by the consumer and never /// moved (because it contains a lot of inline data that would be slow to memmove). /// /// After instantiation, callers may call the push_simple_selector() and push_combinator() methods /// to append selector data as it is encountered (from left to right). Once the process is /// complete, callers should invoke build(), which transforms the contents of the SelectorBuilder /// into a heap- allocated Selector and leaves the builder in a drained state. #[derive(Debug)] pub struct SelectorBuilder { /// The entire sequence of components. We make this large because the result of parsing a /// selector is fed into a new Arc-ed allocation, so any spilled vec would be a wasted /// allocation. Also, Components are large enough that we don't have much cache locality /// benefit from reserving stack space for fewer of them. components: SmallVec<[Component; 32]>, last_compound_start: Option, } impl Push> for SelectorBuilder { fn push(&mut self, value: Component) { self.push_simple_selector(value); } } impl SelectorBuilder { /// Pushes a simple selector onto the current compound selector. #[inline(always)] pub fn push_simple_selector(&mut self, ss: Component) { debug_assert!(!ss.is_combinator()); self.components.push(ss); } /// Completes the current compound selector and starts a new one, delimited by the given /// combinator. #[inline(always)] pub fn push_combinator(&mut self, c: Combinator) { self.reverse_last_compound(); self.components.push(Component::Combinator(c)); self.last_compound_start = Some(self.components.len()); } fn reverse_last_compound(&mut self) { let start = self.last_compound_start.unwrap_or(0); self.components[start..].reverse(); } /// Returns true if combinators have ever been pushed to this builder. #[inline(always)] pub fn has_combinators(&self) -> bool { self.last_compound_start.is_some() } /// Consumes the builder, producing a Selector. #[inline(always)] pub fn build(&mut self, parse_relative: ParseRelative) -> SelectorData { // Compute the specificity and flags. let sf = specificity_and_flags( self.components.iter(), /* for_nesting_parent = */ false, ); self.build_with_specificity_and_flags(sf, parse_relative) } /// Builds with an explicit SpecificityAndFlags. This is separated from build() so that unit /// tests can pass an explicit specificity. #[inline(always)] pub(crate) fn build_with_specificity_and_flags( &mut self, mut spec: SpecificityAndFlags, parse_relative: ParseRelative, ) -> SelectorData { let implicit_addition = match parse_relative { ParseRelative::ForNesting if !spec.flags.intersects(SelectorFlags::HAS_PARENT) => { Some((Component::ParentSelector, SelectorFlags::HAS_PARENT)) }, ParseRelative::ForScope if !spec .flags .intersects(SelectorFlags::HAS_SCOPE | SelectorFlags::HAS_PARENT) => { Some((Component::ImplicitScope, SelectorFlags::HAS_SCOPE)) }, _ => None, }; let implicit_selector_and_combinator; let implicit_selector = if let Some((component, flag)) = implicit_addition { spec.flags.insert(flag); implicit_selector_and_combinator = [Component::Combinator(Combinator::Descendant), component]; &implicit_selector_and_combinator[..] } else { &[] }; // As an optimization, for a selector without combinators, we can just keep the order // as-is. if self.last_compound_start.is_none() { return Arc::from_header_and_iter( spec, ExactChain(self.components.drain(..), implicit_selector.iter().cloned()), ); } self.reverse_last_compound(); Arc::from_header_and_iter( spec, ExactChain( self.components.drain(..).rev(), implicit_selector.iter().cloned(), ), ) } } impl Default for SelectorBuilder { #[inline(always)] fn default() -> Self { SelectorBuilder { components: SmallVec::new(), last_compound_start: None, } } } // This is effectively a Chain<>, but Chain isn't an ExactSizeIterator, see // https://github.com/rust-lang/rust/issues/34433 struct ExactChain(A, B); impl ExactSizeIterator for ExactChain where A: ExactSizeIterator, B: ExactSizeIterator, { fn len(&self) -> usize { self.0.len() + self.1.len() } } impl Iterator for ExactChain where A: ExactSizeIterator, B: ExactSizeIterator, { type Item = Item; #[inline(always)] fn next(&mut self) -> Option { self.0.next().or_else(|| self.1.next()) } fn size_hint(&self) -> (usize, Option) { let len = self.len(); (len, Some(len)) } } /// Flags that indicate at which point of parsing a selector are we. #[derive(Clone, Copy, Default, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub(crate) struct SelectorFlags(u8); bitflags! { impl SelectorFlags: u8 { const HAS_PSEUDO = 1 << 0; const HAS_SLOTTED = 1 << 1; const HAS_PART = 1 << 2; const HAS_PARENT = 1 << 3; const HAS_HOST = 1 << 4; const HAS_SCOPE = 1 << 5; } } impl core::fmt::Debug for SelectorFlags { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.is_empty() { write!(f, "{:#x}", Self::empty().bits()) } else { bitflags::parser::to_writer(self, f) } } } impl SelectorFlags { /// When you nest a pseudo-element with something like: /// /// ::before { & { .. } } /// /// It is not supposed to work, because :is(::before) is invalid. We can't propagate the /// pseudo-flags from inner to outer selectors, to avoid breaking our invariants. pub(crate) fn forbidden_for_nesting() -> Self { Self::HAS_PSEUDO | Self::HAS_SLOTTED | Self::HAS_PART } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub struct SpecificityAndFlags { /// There are two free bits here, since we use ten bits for each specificity /// kind (id, class, element). pub(crate) specificity: u32, /// There's padding after this field due to the size of the flags. pub(crate) flags: SelectorFlags, } const MAX_10BIT: u32 = (1u32 << 10) - 1; #[derive(Add, AddAssign, Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] pub(crate) struct Specificity { id_selectors: u32, class_like_selectors: u32, element_selectors: u32, } impl Specificity { // Return the specficity of a single class-like selector. #[inline] pub fn single_class_like() -> Self { Specificity { id_selectors: 0, class_like_selectors: 1, element_selectors: 0, } } } impl From for Specificity { #[inline] fn from(value: u32) -> Specificity { assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT); Specificity { id_selectors: value >> 20, class_like_selectors: (value >> 10) & MAX_10BIT, element_selectors: value & MAX_10BIT, } } } impl From for u32 { #[inline] fn from(specificity: Specificity) -> u32 { cmp::min(specificity.id_selectors, MAX_10BIT) << 20 | cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10 | cmp::min(specificity.element_selectors, MAX_10BIT) } } fn specificity_and_flags( iter: slice::Iter>, for_nesting_parent: bool, ) -> SpecificityAndFlags where Impl: SelectorImpl, { fn component_specificity( simple_selector: &Component, specificity: &mut Specificity, flags: &mut SelectorFlags, for_nesting_parent: bool, ) where Impl: SelectorImpl, { match *simple_selector { Component::Combinator(..) => {}, Component::ParentSelector => flags.insert(SelectorFlags::HAS_PARENT), Component::Part(..) => { flags.insert(SelectorFlags::HAS_PART); if !for_nesting_parent { specificity.element_selectors += 1 } }, Component::PseudoElement(ref pseudo) => { use crate::parser::PseudoElement; flags.insert(SelectorFlags::HAS_PSEUDO); if !for_nesting_parent { specificity.element_selectors += pseudo.specificity_count(); } }, Component::LocalName(..) => specificity.element_selectors += 1, Component::Slotted(ref selector) => { flags.insert(SelectorFlags::HAS_SLOTTED); if !for_nesting_parent { specificity.element_selectors += 1; // Note that due to the way ::slotted works we only compete with // other ::slotted rules, so the above rule doesn't really // matter, but we do it still for consistency with other // pseudo-elements. // // See: https://github.com/w3c/csswg-drafts/issues/1915 *specificity += Specificity::from(selector.specificity()); } flags.insert(selector.flags()); }, Component::Host(ref selector) => { flags.insert(SelectorFlags::HAS_HOST); specificity.class_like_selectors += 1; if let Some(ref selector) = *selector { // See: https://github.com/w3c/csswg-drafts/issues/1915 *specificity += Specificity::from(selector.specificity()); flags.insert(selector.flags()); } }, Component::ID(..) => { specificity.id_selectors += 1; }, Component::Class(..) | Component::AttributeInNoNamespace { .. } | Component::AttributeInNoNamespaceExists { .. } | Component::AttributeOther(..) | Component::Root | Component::Empty | Component::Nth(..) | Component::NonTSPseudoClass(..) => { specificity.class_like_selectors += 1; }, Component::Scope | Component::ImplicitScope => { flags.insert(SelectorFlags::HAS_SCOPE); if matches!(*simple_selector, Component::Scope) { specificity.class_like_selectors += 1; } }, Component::NthOf(ref nth_of_data) => { // https://drafts.csswg.org/selectors/#specificity-rules: // // The specificity of the :nth-last-child() pseudo-class, // like the :nth-child() pseudo-class, combines the // specificity of a regular pseudo-class with that of its // selector argument S. specificity.class_like_selectors += 1; let sf = selector_list_specificity_and_flags( nth_of_data.selectors().iter(), for_nesting_parent, ); *specificity += Specificity::from(sf.specificity); flags.insert(sf.flags); }, // https://drafts.csswg.org/selectors/#specificity-rules: // // The specificity of an :is(), :not(), or :has() pseudo-class // is replaced by the specificity of the most specific complex // selector in its selector list argument. Component::Where(ref list) | Component::Negation(ref list) | Component::Is(ref list) => { let sf = selector_list_specificity_and_flags( list.slice().iter(), /* nested = */ true, ); if !matches!(*simple_selector, Component::Where(..)) { *specificity += Specificity::from(sf.specificity); } flags.insert(sf.flags); }, Component::Has(ref relative_selectors) => { let sf = relative_selector_list_specificity_and_flags( relative_selectors, for_nesting_parent, ); *specificity += Specificity::from(sf.specificity); flags.insert(sf.flags); }, Component::ExplicitUniversalType | Component::ExplicitAnyNamespace | Component::ExplicitNoNamespace | Component::DefaultNamespace(..) | Component::Namespace(..) | Component::RelativeSelectorAnchor | Component::Invalid(..) => { // Does not affect specificity }, } } let mut specificity = Default::default(); let mut flags = Default::default(); for simple_selector in iter { component_specificity( &simple_selector, &mut specificity, &mut flags, for_nesting_parent, ); } SpecificityAndFlags { specificity: specificity.into(), flags, } } /// Finds the maximum specificity of elements in the list and returns it. pub(crate) fn selector_list_specificity_and_flags<'a, Impl: SelectorImpl>( itr: impl Iterator>, for_nesting_parent: bool, ) -> SpecificityAndFlags { let mut specificity = 0; let mut flags = SelectorFlags::empty(); for selector in itr { let selector_flags = selector.flags(); let selector_specificity = if for_nesting_parent && selector_flags.intersects(SelectorFlags::forbidden_for_nesting()) { // In this case we need to re-compute the specificity. specificity_and_flags(selector.iter_raw_match_order(), for_nesting_parent).specificity } else { selector.specificity() }; specificity = std::cmp::max(specificity, selector_specificity); flags.insert(selector.flags()); } SpecificityAndFlags { specificity, flags } } pub(crate) fn relative_selector_list_specificity_and_flags( list: &[RelativeSelector], for_nesting_parent: bool, ) -> SpecificityAndFlags { selector_list_specificity_and_flags(list.iter().map(|rel| &rel.selector), for_nesting_parent) } ================================================ FILE: selectors/context.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::attr::CaseSensitivity; use crate::bloom::BloomFilter; use crate::nth_index_cache::{NthIndexCache, NthIndexCacheInner}; use crate::parser::{Selector, SelectorImpl}; use crate::relative_selector::cache::RelativeSelectorCache; use crate::relative_selector::filter::RelativeSelectorFilterMap; use crate::tree::{Element, OpaqueElement}; /// What kind of selector matching mode we should use. /// /// There are two modes of selector matching. The difference is only noticeable /// in presence of pseudo-elements. #[derive(Clone, Copy, Debug, PartialEq)] pub enum MatchingMode { /// Don't ignore any pseudo-element selectors. Normal, /// Ignores any stateless pseudo-element selectors in the rightmost sequence /// of simple selectors. /// /// This is useful, for example, to match against ::before when you aren't a /// pseudo-element yourself. /// /// For example, in presence of `::before:hover`, it would never match, but /// `::before` would be ignored as in "matching". /// /// It's required for all the selectors you match using this mode to have a /// pseudo-element. ForStatelessPseudoElement, } /// The mode to use when matching unvisited and visited links. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum VisitedHandlingMode { /// All links are matched as if they are unvisted. AllLinksUnvisited, /// All links are matched as if they are visited and unvisited (both :link /// and :visited match). /// /// This is intended to be used from invalidation code, to be conservative /// about whether we need to restyle a link. AllLinksVisitedAndUnvisited, /// A element's "relevant link" is the element being matched if it is a link /// or the nearest ancestor link. The relevant link is matched as though it /// is visited, and all other links are matched as if they are unvisited. RelevantLinkVisited, } impl VisitedHandlingMode { #[inline] pub fn matches_visited(&self) -> bool { matches!( *self, VisitedHandlingMode::RelevantLinkVisited | VisitedHandlingMode::AllLinksVisitedAndUnvisited ) } #[inline] pub fn matches_unvisited(&self) -> bool { matches!( *self, VisitedHandlingMode::AllLinksUnvisited | VisitedHandlingMode::AllLinksVisitedAndUnvisited ) } } /// Whether we need to set selector invalidation flags on elements for this /// match request. #[derive(Clone, Copy, Debug, PartialEq)] pub enum NeedsSelectorFlags { No, Yes, } /// Whether we're matching in the contect of invalidation. #[derive(Clone, Copy, PartialEq)] pub enum MatchingForInvalidation { No, Yes, YesForComparison, } impl MatchingForInvalidation { /// Are we matching for invalidation? pub fn is_for_invalidation(&self) -> bool { matches!(*self, Self::Yes | Self::YesForComparison) } } /// Which quirks mode is this document in. /// /// See: https://quirks.spec.whatwg.org/ #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum QuirksMode { /// Quirks mode. Quirks, /// Limited quirks mode. LimitedQuirks, /// No quirks mode. NoQuirks, } impl QuirksMode { #[inline] pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity { match self { QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive, QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive, } } } /// Set of caches (And cache-likes) that speed up expensive selector matches. #[derive(Default)] pub struct SelectorCaches { /// A cache to speed up nth-index-like selectors. pub nth_index: NthIndexCache, /// A cache to speed up relative selector matches. See module documentation. pub relative_selector: RelativeSelectorCache, /// A map of bloom filters to fast-reject relative selector matches. pub relative_selector_filter_map: RelativeSelectorFilterMap, } /// Data associated with the matching process for a element. This context is /// used across many selectors for an element, so it's not appropriate for /// transient data that applies to only a single selector. pub struct MatchingContext<'a, Impl> where Impl: SelectorImpl, { /// Input with the matching mode we should use when matching selectors. matching_mode: MatchingMode, /// Input with the bloom filter used to fast-reject selectors. pub bloom_filter: Option<&'a BloomFilter>, /// The element which is going to match :scope pseudo-class. It can be /// either one :scope element, or the scoping element. /// /// Note that, although in theory there can be multiple :scope elements, /// in current specs, at most one is specified, and when there is one, /// scoping element is not relevant anymore, so we use a single field for /// them. /// /// When this is None, :scope will match the root element. /// /// See https://drafts.csswg.org/selectors-4/#scope-pseudo pub scope_element: Option, /// The current shadow host we're collecting :host rules for. pub current_host: Option, /// Controls how matching for links is handled. visited_handling: VisitedHandlingMode, /// Whether we're currently matching a featureless element. pub featureless: bool, /// The current nesting level of selectors that we're matching. nesting_level: usize, /// Whether we're inside a negation or not. in_negation: bool, /// An optional hook function for checking whether a pseudo-element /// should match when matching_mode is ForStatelessPseudoElement. pub pseudo_element_matching_fn: Option<&'a dyn Fn(&Impl::PseudoElement) -> bool>, /// Extra implementation-dependent matching data. pub extra_data: Impl::ExtraMatchingData<'a>, /// The current element we're anchoring on for evaluating the relative selector. current_relative_selector_anchor: Option, quirks_mode: QuirksMode, needs_selector_flags: NeedsSelectorFlags, /// Whether we're matching in the context of invalidation. matching_for_invalidation: MatchingForInvalidation, /// Whether we're matching in the context of revalidation. matching_for_revalidation: bool, /// Caches to speed up expensive selector matches. pub selector_caches: &'a mut SelectorCaches, classes_and_ids_case_sensitivity: CaseSensitivity, _impl: ::std::marker::PhantomData, } impl<'a, Impl> MatchingContext<'a, Impl> where Impl: SelectorImpl, { /// Constructs a new `MatchingContext`. pub fn new( matching_mode: MatchingMode, bloom_filter: Option<&'a BloomFilter>, selector_caches: &'a mut SelectorCaches, quirks_mode: QuirksMode, needs_selector_flags: NeedsSelectorFlags, matching_for_invalidation: MatchingForInvalidation, ) -> Self { Self::new_internal( matching_mode, bloom_filter, selector_caches, VisitedHandlingMode::AllLinksUnvisited, quirks_mode, needs_selector_flags, matching_for_invalidation, false, ) } /// Constructs a new `MatchingContext` for revalidation. pub fn new_for_revalidation( bloom_filter: Option<&'a BloomFilter>, selector_caches: &'a mut SelectorCaches, quirks_mode: QuirksMode, needs_selector_flags: NeedsSelectorFlags, ) -> Self { Self::new_internal( // NB: `MatchingMode` doesn't really matter, given we don't share style // between pseudos. MatchingMode::Normal, bloom_filter, selector_caches, VisitedHandlingMode::AllLinksUnvisited, quirks_mode, needs_selector_flags, MatchingForInvalidation::No, true, ) } /// Constructs a new `MatchingContext` for use in visited matching. pub fn new_for_visited( matching_mode: MatchingMode, bloom_filter: Option<&'a BloomFilter>, selector_caches: &'a mut SelectorCaches, visited_handling: VisitedHandlingMode, quirks_mode: QuirksMode, needs_selector_flags: NeedsSelectorFlags, matching_for_invalidation: MatchingForInvalidation, ) -> Self { Self::new_internal( matching_mode, bloom_filter, selector_caches, visited_handling, quirks_mode, needs_selector_flags, matching_for_invalidation, false, ) } fn new_internal( matching_mode: MatchingMode, bloom_filter: Option<&'a BloomFilter>, selector_caches: &'a mut SelectorCaches, visited_handling: VisitedHandlingMode, quirks_mode: QuirksMode, needs_selector_flags: NeedsSelectorFlags, matching_for_invalidation: MatchingForInvalidation, matching_for_revalidation: bool, ) -> Self { Self { matching_mode, bloom_filter, visited_handling, quirks_mode, classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(), needs_selector_flags, matching_for_invalidation, matching_for_revalidation, scope_element: None, current_host: None, featureless: false, nesting_level: 0, in_negation: false, pseudo_element_matching_fn: None, extra_data: Default::default(), current_relative_selector_anchor: None, selector_caches, _impl: ::std::marker::PhantomData, } } // Grab a reference to the appropriate cache. #[inline] pub fn nth_index_cache( &mut self, is_of_type: bool, is_from_end: bool, selectors: &[Selector], ) -> &mut NthIndexCacheInner { self.selector_caches .nth_index .get(is_of_type, is_from_end, selectors) } /// Whether we're matching a nested selector. #[inline] pub fn is_nested(&self) -> bool { self.nesting_level != 0 } /// Whether we're matching inside a :not(..) selector. #[inline] pub fn in_negation(&self) -> bool { self.in_negation } /// The quirks mode of the document. #[inline] pub fn quirks_mode(&self) -> QuirksMode { self.quirks_mode } /// The matching-mode for this selector-matching operation. #[inline] pub fn matching_mode(&self) -> MatchingMode { self.matching_mode } /// Whether we need to set selector flags. #[inline] pub fn needs_selector_flags(&self) -> bool { self.needs_selector_flags == NeedsSelectorFlags::Yes } /// Whether or not we're matching to invalidate. #[inline] pub fn matching_for_invalidation(&self) -> bool { self.matching_for_invalidation.is_for_invalidation() } /// Whether or not we're matching to revalidate. #[inline] pub fn matching_for_revalidation(&self) -> bool { self.matching_for_revalidation } /// Whether or not we're comparing for invalidation, if we are matching for invalidation. #[inline] pub fn matching_for_invalidation_comparison(&self) -> Option { match self.matching_for_invalidation { MatchingForInvalidation::No => None, MatchingForInvalidation::Yes => Some(false), MatchingForInvalidation::YesForComparison => Some(true), } } /// Run the given matching function for before/after invalidation comparison. #[inline] pub fn for_invalidation_comparison(&mut self, f: F) -> R where F: FnOnce(&mut Self) -> R, { debug_assert!( self.matching_for_invalidation(), "Not matching for invalidation?" ); let prev = self.matching_for_invalidation; self.matching_for_invalidation = MatchingForInvalidation::YesForComparison; let result = f(self); self.matching_for_invalidation = prev; result } /// The case-sensitivity for class and ID selectors #[inline] pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity { self.classes_and_ids_case_sensitivity } /// Runs F with a deeper nesting level. #[inline] pub fn nest(&mut self, f: F) -> R where F: FnOnce(&mut Self) -> R, { self.nesting_level += 1; let result = f(self); self.nesting_level -= 1; result } /// Runs F with a deeper nesting level, and marking ourselves in a negation, /// for a :not(..) selector, for example. #[inline] pub fn nest_for_negation(&mut self, f: F) -> R where F: FnOnce(&mut Self) -> R, { let old_in_negation = self.in_negation; self.in_negation = !self.in_negation; let result = self.nest(f); self.in_negation = old_in_negation; result } #[inline] pub fn visited_handling(&self) -> VisitedHandlingMode { self.visited_handling } /// Runs F with a different featureless element flag. #[inline] pub fn with_featureless(&mut self, featureless: bool, f: F) -> R where F: FnOnce(&mut Self) -> R, { let orig = self.featureless; self.featureless = featureless; let result = f(self); self.featureless = orig; result } /// Returns whether the currently matching element is acting as a featureless element (e.g., /// because we've crossed a shadow boundary). This is used to implement the :host selector /// rules properly. #[inline] pub fn featureless(&self) -> bool { self.featureless } /// Runs F with a different VisitedHandlingMode. #[inline] pub fn with_visited_handling_mode( &mut self, handling_mode: VisitedHandlingMode, f: F, ) -> R where F: FnOnce(&mut Self) -> R, { let original_handling_mode = self.visited_handling; self.visited_handling = handling_mode; let result = f(self); self.visited_handling = original_handling_mode; result } /// Runs F with a given shadow host which is the root of the tree whose /// rules we're matching. #[inline] pub fn with_shadow_host(&mut self, host: Option, f: F) -> R where E: Element, F: FnOnce(&mut Self) -> R, { let original_host = self.current_host.take(); self.current_host = host.map(|h| h.opaque()); let result = f(self); self.current_host = original_host; result } /// Returns the current shadow host whose shadow root we're matching rules /// against. #[inline] pub fn shadow_host(&self) -> Option { self.current_host } /// Runs F with a deeper nesting level, with the given element as the anchor, /// for a :has(...) selector, for example. #[inline] pub fn nest_for_relative_selector(&mut self, anchor: OpaqueElement, f: F) -> R where F: FnOnce(&mut Self) -> R, { debug_assert!( self.current_relative_selector_anchor.is_none(), "Nesting should've been rejected at parse time" ); self.current_relative_selector_anchor = Some(anchor); let result = self.nest(f); self.current_relative_selector_anchor = None; result } /// Runs F with a deeper nesting level, with the given element as the scope. #[inline] pub fn nest_for_scope(&mut self, scope: Option, f: F) -> R where F: FnOnce(&mut Self) -> R, { let original_scope_element = self.scope_element; self.scope_element = scope; let result = f(self); self.scope_element = original_scope_element; result } /// Runs F with a deeper nesting level, with the given element as the scope, for /// matching `scope-start` and/or `scope-end` conditions. #[inline] pub fn nest_for_scope_condition(&mut self, scope: Option, f: F) -> R where F: FnOnce(&mut Self) -> R, { let original_matching_mode = self.matching_mode; // We may as well be matching for a pseudo-element inside `@scope`, but // the scope-defining selectors wouldn't be matching them. self.matching_mode = MatchingMode::Normal; let result = self.nest_for_scope(scope, f); self.matching_mode = original_matching_mode; result } /// Returns the current anchor element to evaluate the relative selector against. #[inline] pub fn relative_selector_anchor(&self) -> Option { self.current_relative_selector_anchor } } ================================================ FILE: selectors/kleene_value.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Kleen logic: https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics /// A "trilean" value based on Kleen logic. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum KleeneValue { /// False False = 0, /// True True = 1, /// Either true or false, but we’re not sure which yet. Unknown, } impl From for KleeneValue { fn from(b: bool) -> Self { if b { Self::True } else { Self::False } } } impl KleeneValue { /// Turns this Kleene value to a bool, taking the unknown value as an /// argument. pub fn to_bool(self, unknown: bool) -> bool { match self { Self::True => true, Self::False => false, Self::Unknown => unknown, } } /// Return true if any result of f() is definitely true. /// Otherwise, return the `or` of all values. /// Returns false if empty, like that of `Iterator`. #[inline(always)] pub fn any(iter: impl Iterator, f: impl FnMut(T) -> Self) -> Self { Self::any_value(iter, Self::True, Self::False, |a, b| a | b, f) } /// Return false if any results of f() is definitely false. /// Otherwise, return the `and` of all values. /// Returns true if empty, opposite of `Iterator`. #[inline(always)] pub fn any_false(iter: impl Iterator, f: impl FnMut(T) -> Self) -> Self { Self::any_value(iter, Self::False, Self::True, |a, b| a & b, f) } #[inline(always)] fn any_value( iter: impl Iterator, value: Self, on_empty: Self, op: impl Fn(Self, Self) -> Self, mut f: impl FnMut(T) -> Self, ) -> Self { let mut result = None; for item in iter { let r = f(item); if r == value { return r; } if let Some(v) = result.as_mut() { *v = op(*v, r); } else { result = Some(r); } } result.unwrap_or(on_empty) } } impl std::ops::Not for KleeneValue { type Output = Self; fn not(self) -> Self { match self { Self::True => Self::False, Self::False => Self::True, Self::Unknown => Self::Unknown, } } } // Implements the logical and operation. impl std::ops::BitAnd for KleeneValue { type Output = Self; fn bitand(self, other: Self) -> Self { if self == Self::False || other == Self::False { return Self::False; } if self == Self::Unknown || other == Self::Unknown { return Self::Unknown; } Self::True } } // Implements the logical or operation. impl std::ops::BitOr for KleeneValue { type Output = Self; fn bitor(self, other: Self) -> Self { if self == Self::True || other == Self::True { return Self::True; } if self == Self::Unknown || other == Self::Unknown { return Self::Unknown; } Self::False } } impl std::ops::BitOrAssign for KleeneValue { fn bitor_assign(&mut self, other: Self) { *self = *self | other; } } impl std::ops::BitAndAssign for KleeneValue { fn bitand_assign(&mut self, other: Self) { *self = *self & other; } } ================================================ FILE: selectors/lib.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Make |cargo bench| work. #![cfg_attr(feature = "bench", feature(test))] pub mod attr; pub mod bloom; mod builder; pub mod context; pub mod kleene_value; pub mod matching; mod nth_index_cache; pub mod parser; pub mod relative_selector; pub mod sink; mod tree; pub mod visitor; pub use crate::nth_index_cache::NthIndexCache; pub use crate::parser::{Parser, SelectorImpl, SelectorList}; pub use crate::tree::{Element, OpaqueElement}; ================================================ FILE: selectors/matching.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::attr::{ AttrSelectorOperation, AttrSelectorWithOptionalNamespace, CaseSensitivity, NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity, }; use crate::bloom::{BloomFilter, BLOOM_HASH_MASK}; use crate::kleene_value::KleeneValue; use crate::parser::{ AncestorHashes, Combinator, Component, LocalName, MatchesFeaturelessHost, NthSelectorData, RelativeSelectorMatchHint, }; use crate::parser::{ NonTSPseudoClass, RelativeSelector, Selector, SelectorImpl, SelectorIter, SelectorList, }; use crate::relative_selector::cache::RelativeSelectorCachedMatch; use crate::tree::Element; use bitflags::bitflags; use debug_unreachable::debug_unreachable; use log::debug; use smallvec::SmallVec; use std::borrow::Borrow; pub use crate::context::*; // The bloom filter for descendant CSS selectors will have a <1% false // positive rate until it has this many selectors in it, then it will // rapidly increase. pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096; bitflags! { /// Set of flags that are set on either the element or its parent (depending /// on the flag) if the element could potentially match a selector. #[derive(Clone, Copy)] pub struct ElementSelectorFlags: usize { /// When a child is added or removed from the parent, all the children /// must be restyled, because they may match :nth-last-child, /// :last-of-type, :nth-last-of-type, or :only-of-type. const HAS_SLOW_SELECTOR = 1 << 0; /// When a child is added or removed from the parent, any later /// children must be restyled, because they may match :nth-child, /// :first-of-type, or :nth-of-type. const HAS_SLOW_SELECTOR_LATER_SIBLINGS = 1 << 1; /// HAS_SLOW_SELECTOR* was set by the presence of :nth (But not of). const HAS_SLOW_SELECTOR_NTH = 1 << 2; /// When a DOM mutation occurs on a child that might be matched by /// :nth-last-child(.. of ), earlier children must be /// restyled, and HAS_SLOW_SELECTOR will be set (which normally /// indicates that all children will be restyled). /// /// Similarly, when a DOM mutation occurs on a child that might be /// matched by :nth-child(.. of ), later children must be /// restyled, and HAS_SLOW_SELECTOR_LATER_SIBLINGS will be set. const HAS_SLOW_SELECTOR_NTH_OF = 1 << 3; /// When a child is added or removed from the parent, the first and /// last children must be restyled, because they may match :first-child, /// :last-child, or :only-child. const HAS_EDGE_CHILD_SELECTOR = 1 << 4; /// The element has an empty selector, so when a child is appended we /// might need to restyle the parent completely. const HAS_EMPTY_SELECTOR = 1 << 5; /// The element may anchor a relative selector. const ANCHORS_RELATIVE_SELECTOR = 1 << 6; /// The element may anchor a relative selector that is not the subject /// of the whole selector. const ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT = 1 << 7; /// The element is reached by a relative selector search in the sibling direction. const RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING = 1 << 8; /// The element is reached by a relative selector search in the ancestor direction. const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR = 1 << 9; // The element is reached by a relative selector search in both sibling and ancestor directions. const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING = Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING.bits() | Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR.bits(); } } impl ElementSelectorFlags { /// Returns the subset of flags that apply to the element. pub fn for_self(self) -> ElementSelectorFlags { self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR | ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR | ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) } /// Returns the subset of flags that apply to the parent. pub fn for_parent(self) -> ElementSelectorFlags { self & (ElementSelectorFlags::HAS_SLOW_SELECTOR | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS | ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH | ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) } } /// Holds per-compound-selector data. struct LocalMatchingContext<'a, 'b: 'a, Impl: SelectorImpl> { shared: &'a mut MatchingContext<'b, Impl>, rightmost: SubjectOrPseudoElement, quirks_data: Option>, } #[inline(always)] pub fn matches_selector_list( selector_list: &SelectorList, element: &E, context: &mut MatchingContext, ) -> bool where E: Element, { // This is pretty much any(..) but manually inlined because the compiler // refuses to do so from querySelector / querySelectorAll. for selector in selector_list.slice() { let matches = matches_selector(selector, 0, None, element, context); if matches { return true; } } false } /// Given the ancestor hashes from a selector, see if the current element, /// represented by the bloom filter, has a chance of matching at all. #[inline(always)] pub fn selector_may_match(hashes: &AncestorHashes, bf: &BloomFilter) -> bool { // Check the first three hashes. Note that we can check for zero before // masking off the high bits, since if any of the first three hashes is // zero the fourth will be as well. We also take care to avoid the // special-case complexity of the fourth hash until we actually reach it, // because we usually don't. // // To be clear: this is all extremely hot. for i in 0..3 { let packed = hashes.packed_hashes[i]; if packed == 0 { // No more hashes left - unable to fast-reject. return true; } if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) { // Hooray! We fast-rejected on this hash. return false; } } // Now do the slighty-more-complex work of synthesizing the fourth hash, // and check it against the filter if it exists. let fourth = hashes.fourth_hash(); fourth == 0 || bf.might_contain_hash(fourth) } /// A result of selector matching, includes 3 failure types, /// /// NotMatchedAndRestartFromClosestLaterSibling /// NotMatchedAndRestartFromClosestDescendant /// NotMatchedGlobally /// /// When NotMatchedGlobally appears, stop selector matching completely since /// the succeeding selectors never matches. /// It is raised when /// Child combinator cannot find the candidate element. /// Descendant combinator cannot find the candidate element. /// /// When NotMatchedAndRestartFromClosestDescendant appears, the selector /// matching does backtracking and restarts from the closest Descendant /// combinator. /// It is raised when /// NextSibling combinator cannot find the candidate element. /// LaterSibling combinator cannot find the candidate element. /// Child combinator doesn't match on the found element. /// /// When NotMatchedAndRestartFromClosestLaterSibling appears, the selector /// matching does backtracking and restarts from the closest LaterSibling /// combinator. /// It is raised when /// NextSibling combinator doesn't match on the found element. /// /// For example, when the selector "d1 d2 a" is provided and we cannot *find* /// an appropriate ancestor element for "d1", this selector matching raises /// NotMatchedGlobally since even if "d2" is moved to more upper element, the /// candidates for "d1" becomes less than before and d1 . /// /// The next example is siblings. When the selector "b1 + b2 ~ d1 a" is /// provided and we cannot *find* an appropriate brother element for b1, /// the selector matching raises NotMatchedAndRestartFromClosestDescendant. /// The selectors ("b1 + b2 ~") doesn't match and matching restart from "d1". /// /// The additional example is child and sibling. When the selector /// "b1 + c1 > b2 ~ d1 a" is provided and the selector "b1" doesn't match on /// the element, this "b1" raises NotMatchedAndRestartFromClosestLaterSibling. /// However since the selector "c1" raises /// NotMatchedAndRestartFromClosestDescendant. So the selector /// "b1 + c1 > b2 ~ " doesn't match and restart matching from "d1". /// /// There is also the unknown result, which is used during invalidation when /// specific selector is being tested for before/after comparison. More specifically, /// selectors that are too expensive to correctly compute during invalidation may /// return unknown, as the computation will be thrown away and only to be recomputed /// during styling. For most cases, the unknown result can be treated as matching. /// This is because a compound of selectors acts like &&, and unknown && matched /// == matched and unknown && not-matched == not-matched. However, some selectors, /// like `:is()`, behave like || i.e. `:is(.a, .b)` == a || b. Treating unknown /// == matching then causes these selectors to always return matching, which undesired /// for before/after comparison. Coercing to not-matched doesn't work since each /// inner selector may have compounds: e.g. Toggling `.foo` in `:is(.foo:has(..))` /// with coersion to not-matched would result in an invalid before/after comparison /// of not-matched/not-matched. #[derive(Clone, Copy, Eq, PartialEq)] enum SelectorMatchingResult { Matched, NotMatchedAndRestartFromClosestLaterSibling, NotMatchedAndRestartFromClosestDescendant, NotMatchedGlobally, Unknown, } impl From for KleeneValue { #[inline] fn from(value: SelectorMatchingResult) -> Self { match value { SelectorMatchingResult::Matched => KleeneValue::True, SelectorMatchingResult::Unknown => KleeneValue::Unknown, SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling | SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant | SelectorMatchingResult::NotMatchedGlobally => KleeneValue::False, } } } /// Matches a selector, fast-rejecting against a bloom filter. /// /// We accept an offset to allow consumers to represent and match against /// partial selectors (indexed from the right). We use this API design, rather /// than having the callers pass a SelectorIter, because creating a SelectorIter /// requires dereferencing the selector to get the length, which adds an /// unnecessary cache miss for cases when we can fast-reject with AncestorHashes /// (which the caller can store inline with the selector pointer). #[inline(always)] pub fn matches_selector( selector: &Selector, offset: usize, hashes: Option<&AncestorHashes>, element: &E, context: &mut MatchingContext, ) -> bool where E: Element, { let result = matches_selector_kleene(selector, offset, hashes, element, context); if cfg!(debug_assertions) && result == KleeneValue::Unknown { debug_assert!( context .matching_for_invalidation_comparison() .unwrap_or(false), "How did we return unknown?" ); } result.to_bool(true) } /// Same as matches_selector, but returns the Kleene value as-is. #[inline(always)] pub fn matches_selector_kleene( selector: &Selector, offset: usize, hashes: Option<&AncestorHashes>, element: &E, context: &mut MatchingContext, ) -> KleeneValue where E: Element, { // Use the bloom filter to fast-reject. if let Some(hashes) = hashes { if let Some(filter) = context.bloom_filter { if !selector_may_match(hashes, filter) { return KleeneValue::False; } } } matches_complex_selector( selector.iter_from(offset), element, context, if selector.is_rightmost(offset) { SubjectOrPseudoElement::Yes } else { SubjectOrPseudoElement::No }, ) } /// Whether a compound selector matched, and whether it was the rightmost /// selector inside the complex selector. pub enum CompoundSelectorMatchingResult { /// The selector was fully matched. FullyMatched, /// The compound selector matched, and the next combinator offset is /// `next_combinator_offset`. Matched { next_combinator_offset: usize }, /// The selector didn't match. NotMatched, } fn complex_selector_early_reject_by_local_name( list: &SelectorList, element: &E, ) -> bool { list.slice() .iter() .all(|s| early_reject_by_local_name(s, 0, element)) } /// Returns true if this compound would not match the given element by due /// to a local name selector (If one exists). pub fn early_reject_by_local_name( selector: &Selector, from_offset: usize, element: &E, ) -> bool { let iter = selector.iter_from(from_offset); for component in iter { if match component { Component::LocalName(name) => !matches_local_name(element, name), Component::Is(list) | Component::Where(list) => { complex_selector_early_reject_by_local_name(list, element) }, _ => continue, } { return true; } } false } /// Matches a compound selector belonging to `selector`, starting at offset /// `from_offset`, matching left to right. /// /// Requires that `from_offset` points to a `Combinator`. /// /// NOTE(emilio): This doesn't allow to match in the leftmost sequence of the /// complex selector, but it happens to be the case we don't need it. pub fn matches_compound_selector_from( selector: &Selector, mut from_offset: usize, context: &mut MatchingContext, element: &E, ) -> CompoundSelectorMatchingResult where E: Element, { debug_assert!( !context .matching_for_invalidation_comparison() .unwrap_or(false), "CompoundSelectorMatchingResult doesn't support unknown" ); if cfg!(debug_assertions) && from_offset != 0 { selector.combinator_at_parse_order(from_offset - 1); // This asserts. } let mut local_context = LocalMatchingContext { shared: context, // We have no info if this is an outer selector. This function is called in // an invalidation context, which only calls this for non-subject (i.e. // Non-rightmost) positions. rightmost: SubjectOrPseudoElement::No, quirks_data: None, }; // Find the end of the selector or the next combinator, then match // backwards, so that we match in the same order as // matches_complex_selector, which is usually faster. let start_offset = from_offset; for component in selector.iter_raw_parse_order_from(from_offset) { if matches!(*component, Component::Combinator(..)) { debug_assert_ne!(from_offset, 0, "Selector started with a combinator?"); break; } from_offset += 1; } debug_assert!(from_offset >= 1); debug_assert!(from_offset <= selector.len()); let iter = selector.iter_from(selector.len() - from_offset); debug_assert!( iter.clone().next().is_some() || from_offset != selector.len(), "Got the math wrong: {:?} | {:?} | {} {}", selector, selector.iter_raw_match_order().as_slice(), from_offset, start_offset ); debug_assert!( !local_context.shared.featureless(), "Invalidating featureless element somehow?" ); for component in iter { let result = matches_simple_selector(component, element, &mut local_context); debug_assert!( result != KleeneValue::Unknown, "Returned unknown in non invalidation context?" ); if !result.to_bool(true) { return CompoundSelectorMatchingResult::NotMatched; } } if from_offset != selector.len() { return CompoundSelectorMatchingResult::Matched { next_combinator_offset: from_offset, }; } CompoundSelectorMatchingResult::FullyMatched } /// Matches a complex selector. #[inline(always)] fn matches_complex_selector( mut iter: SelectorIter, element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue where E: Element, { // If this is the special pseudo-element mode, consume the ::pseudo-element // before proceeding, since the caller has already handled that part. if context.matching_mode() == MatchingMode::ForStatelessPseudoElement && !context.is_nested() { // Consume the pseudo. match *iter.next().unwrap() { Component::PseudoElement(ref pseudo) => { if let Some(ref f) = context.pseudo_element_matching_fn { if !f(pseudo) { return KleeneValue::False; } } }, ref other => { debug_assert!( false, "Used MatchingMode::ForStatelessPseudoElement \ in a non-pseudo selector {:?}", other ); return KleeneValue::False; }, } if !iter.matches_for_stateless_pseudo_element() { return KleeneValue::False; } // Advance to the non-pseudo-element part of the selector. let next_sequence = iter.next_sequence().unwrap(); debug_assert_eq!(next_sequence, Combinator::PseudoElement); } matches_complex_selector_internal( iter, element, context, rightmost, SubjectOrPseudoElement::Yes, ) .into() } /// Matches each selector of a list as a complex selector fn matches_complex_selector_list( list: &[Selector], element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue { KleeneValue::any(list.iter(), |selector| { matches_complex_selector(selector.iter(), element, context, rightmost) }) } fn matches_relative_selector( relative_selector: &RelativeSelector, element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> bool { // Overall, we want to mark the path that we've traversed so that when an element // is invalidated, we early-reject unnecessary relative selector invalidations. if relative_selector.match_hint.is_descendant_direction() { if context.needs_selector_flags() { element.apply_selector_flags( ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, ); } let mut next_element = element.first_element_child(); while let Some(el) = next_element { if context.needs_selector_flags() { el.apply_selector_flags( ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, ); } let mut matched = matches_complex_selector( relative_selector.selector.iter(), &el, context, rightmost, ) .to_bool(true); if !matched && relative_selector.match_hint.is_subtree() { matched = matches_relative_selector_subtree( &relative_selector.selector, &el, context, rightmost, ); } if matched { return true; } next_element = el.next_sibling_element(); } } else { debug_assert!( matches!( relative_selector.match_hint, RelativeSelectorMatchHint::InNextSibling | RelativeSelectorMatchHint::InNextSiblingSubtree | RelativeSelectorMatchHint::InSibling | RelativeSelectorMatchHint::InSiblingSubtree ), "Not descendant direction, but also not sibling direction?" ); if context.needs_selector_flags() { element.apply_selector_flags( ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, ); } let sibling_flag = if relative_selector.match_hint.is_subtree() { ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING } else { ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING }; let mut next_element = element.next_sibling_element(); while let Some(el) = next_element { if context.needs_selector_flags() { el.apply_selector_flags(sibling_flag); } let matched = if relative_selector.match_hint.is_subtree() { matches_relative_selector_subtree( &relative_selector.selector, &el, context, rightmost, ) } else { matches_complex_selector(relative_selector.selector.iter(), &el, context, rightmost) .to_bool(true) }; if matched { return true; } if relative_selector.match_hint.is_next_sibling() { break; } next_element = el.next_sibling_element(); } } return false; } fn relative_selector_match_early( selector: &RelativeSelector, element: &E, context: &mut MatchingContext, ) -> Option { // See if we can return a cached result. if let Some(cached) = context .selector_caches .relative_selector .lookup(element.opaque(), selector) { return Some(cached.matched()); } // See if we can fast-reject. if context .selector_caches .relative_selector_filter_map .fast_reject(element, selector, context.quirks_mode()) { // Alright, add as unmatched to cache. context.selector_caches.relative_selector.add( element.opaque(), selector, RelativeSelectorCachedMatch::NotMatched, ); return Some(false); } None } fn match_relative_selectors( selectors: &[RelativeSelector], element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue { if context.relative_selector_anchor().is_some() { // FIXME(emilio): This currently can happen with nesting, and it's not fully // correct, arguably. But the ideal solution isn't super-clear either. For now, // cope with it and explicitly reject it at match time. See [1] for discussion. // // [1]: https://github.com/w3c/csswg-drafts/issues/9600 return KleeneValue::False; } if let Some(may_return_unknown) = context.matching_for_invalidation_comparison() { // In the context of invalidation, :has is expensive, especially because we // can't use caching/filtering due to now/then matches. DOM structure also // may have changed. return if may_return_unknown { KleeneValue::Unknown } else { KleeneValue::from(!context.in_negation()) }; } context .nest_for_relative_selector(element.opaque(), |context| { do_match_relative_selectors(selectors, element, context, rightmost) }) .into() } /// Matches a relative selector in a list of relative selectors. fn do_match_relative_selectors( selectors: &[RelativeSelector], element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> bool { // Due to style sharing implications (See style sharing code), we mark the current styling context // to mark elements considered for :has matching. Additionally, we want to mark the elements themselves, // since we don't want to indiscriminately invalidate every element as a potential anchor. if rightmost == SubjectOrPseudoElement::Yes { if context.needs_selector_flags() { element.apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR); } } else { if context.needs_selector_flags() { element .apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT); } } for relative_selector in selectors.iter() { if let Some(result) = relative_selector_match_early(relative_selector, element, context) { if result { return true; } // Early return indicates no match, continue to next selector. continue; } let matched = matches_relative_selector(relative_selector, element, context, rightmost); context.selector_caches.relative_selector.add( element.opaque(), relative_selector, if matched { RelativeSelectorCachedMatch::Matched } else { RelativeSelectorCachedMatch::NotMatched }, ); if matched { return true; } } false } fn matches_relative_selector_subtree( selector: &Selector, element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> bool { let mut current = element.first_element_child(); while let Some(el) = current { if context.needs_selector_flags() { el.apply_selector_flags( ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, ); } if matches_complex_selector(selector.iter(), &el, context, rightmost).to_bool(true) { return true; } if matches_relative_selector_subtree(selector, &el, context, rightmost) { return true; } current = el.next_sibling_element(); } false } /// Whether the :hover and :active quirk applies. /// /// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk fn hover_and_active_quirk_applies( selector_iter: &SelectorIter, context: &MatchingContext, rightmost: SubjectOrPseudoElement, ) -> bool { debug_assert_eq!(context.quirks_mode(), QuirksMode::Quirks); if context.is_nested() { return false; } // This compound selector had a pseudo-element to the right that we // intentionally skipped. if rightmost == SubjectOrPseudoElement::Yes && context.matching_mode() == MatchingMode::ForStatelessPseudoElement { return false; } selector_iter.clone().all(|simple| match *simple { Component::NonTSPseudoClass(ref pseudo_class) => pseudo_class.is_active_or_hover(), _ => false, }) } #[derive(Clone, Copy, PartialEq)] enum SubjectOrPseudoElement { Yes, No, } fn host_for_part(element: &E, context: &MatchingContext) -> Option where E: Element, { let scope = context.current_host; let mut curr = element.containing_shadow_host()?; if scope == Some(curr.opaque()) { return Some(curr); } loop { let parent = curr.containing_shadow_host(); if parent.as_ref().map(|h| h.opaque()) == scope { return Some(curr); } curr = parent?; } } fn assigned_slot(element: &E, context: &MatchingContext) -> Option where E: Element, { debug_assert!(element .assigned_slot() .map_or(true, |s| s.is_html_slot_element())); let scope = context.current_host?; let mut current_slot = element.assigned_slot()?; while current_slot.containing_shadow_host().unwrap().opaque() != scope { current_slot = current_slot.assigned_slot()?; } Some(current_slot) } struct NextElement { next_element: Option, featureless: bool, } impl NextElement { #[inline(always)] fn new(next_element: Option, featureless: bool) -> Self { Self { next_element, featureless, } } } #[inline(always)] fn next_element_for_combinator( element: &E, combinator: Combinator, context: &MatchingContext, ) -> NextElement where E: Element, { match combinator { Combinator::NextSibling | Combinator::LaterSibling => { NextElement::new(element.prev_sibling_element(), false) }, Combinator::Child | Combinator::Descendant => { if let Some(parent) = element.parent_element() { return NextElement::new(Some(parent), false); } let element = if element.parent_node_is_shadow_root() { element.containing_shadow_host() } else { None }; NextElement::new(element, true) }, Combinator::Part => NextElement::new(host_for_part(element, context), false), Combinator::SlotAssignment => NextElement::new(assigned_slot(element, context), false), Combinator::PseudoElement => { NextElement::new(element.pseudo_element_originating_element(), false) }, } } fn matches_complex_selector_internal( mut selector_iter: SelectorIter, element: &E, context: &mut MatchingContext, mut rightmost: SubjectOrPseudoElement, mut first_subject_compound: SubjectOrPseudoElement, ) -> SelectorMatchingResult where E: Element, { debug!( "Matching complex selector {:?} for {:?}", selector_iter, element ); let matches_compound_selector = matches_compound_selector(&mut selector_iter, element, context, rightmost); let Some(combinator) = selector_iter.next_sequence() else { return match matches_compound_selector { KleeneValue::True => SelectorMatchingResult::Matched, KleeneValue::Unknown => SelectorMatchingResult::Unknown, KleeneValue::False => { SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling }, }; }; let is_pseudo_combinator = combinator.is_pseudo_element(); if context.featureless() && !is_pseudo_combinator { // A featureless element shouldn't match any further combinator. // TODO(emilio): Maybe we could avoid the compound matching more eagerly. return SelectorMatchingResult::NotMatchedGlobally; } let is_sibling_combinator = combinator.is_sibling(); if is_sibling_combinator && context.needs_selector_flags() { // We need the flags even if we don't match. element.apply_selector_flags(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS); } if matches_compound_selector == KleeneValue::False { // We don't short circuit unknown here, since the rest of the selector // to the left of this compound may still return false. return SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling; } if !is_pseudo_combinator { rightmost = SubjectOrPseudoElement::No; first_subject_compound = SubjectOrPseudoElement::No; } // Stop matching :visited as soon as we find a link, or a combinator for // something that isn't an ancestor. let mut visited_handling = if is_sibling_combinator { VisitedHandlingMode::AllLinksUnvisited } else { context.visited_handling() }; let candidate_not_found = if is_sibling_combinator { SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant } else { SelectorMatchingResult::NotMatchedGlobally }; let mut element = element.clone(); loop { if element.is_link() { visited_handling = VisitedHandlingMode::AllLinksUnvisited; } let NextElement { next_element, featureless, } = next_element_for_combinator(&element, combinator, &context); element = match next_element { None => return candidate_not_found, Some(e) => e, }; let result = context.with_visited_handling_mode(visited_handling, |context| { context.with_featureless(featureless, |context| { matches_complex_selector_internal( selector_iter.clone(), &element, context, rightmost, first_subject_compound, ) }) }); // Return the status immediately if it is one of the global states. match result { SelectorMatchingResult::Matched => { debug_assert!( matches_compound_selector.to_bool(true), "Compound didn't match?" ); if !matches_compound_selector.to_bool(false) { return SelectorMatchingResult::Unknown; } return result; }, SelectorMatchingResult::Unknown | SelectorMatchingResult::NotMatchedGlobally => { return result }, _ => {}, } match combinator { Combinator::Descendant => { // The Descendant combinator and the status is // NotMatchedAndRestartFromClosestLaterSibling or // NotMatchedAndRestartFromClosestDescendant, or the LaterSibling combinator and // the status is NotMatchedAndRestartFromClosestDescendant, we can continue to // matching on the next candidate element. }, Combinator::Child => { // Upgrade the failure status to NotMatchedAndRestartFromClosestDescendant. return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant; }, Combinator::LaterSibling => { // If the failure status is NotMatchedAndRestartFromClosestDescendant and combinator is // LaterSibling, give up this LaterSibling matching and restart from the closest // descendant combinator. if matches!( result, SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant ) { return result; } }, Combinator::NextSibling | Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => { // NOTE(emilio): Conceptually, PseudoElement / Part / SlotAssignment should return // `candidate_not_found`, but it doesn't matter in practice since they don't have // sibling / descendant combinators to the right of them. This hopefully saves one // branch. return result; }, } if featureless { // A featureless element didn't match the selector, we can stop matching now rather // than looking at following elements for our combinator. return candidate_not_found; } } } #[inline] fn matches_local_name(element: &E, local_name: &LocalName) -> bool where E: Element, { let name = select_name(element, &local_name.name, &local_name.lower_name).borrow(); element.has_local_name(name) } fn matches_part( element: &E, parts: &[::Identifier], context: &mut MatchingContext, ) -> bool where E: Element, { let mut hosts = SmallVec::<[E; 4]>::new(); let mut host = match element.containing_shadow_host() { Some(h) => h, None => return false, }; let current_host = context.current_host; if current_host != Some(host.opaque()) { loop { let outer_host = host.containing_shadow_host(); if outer_host.as_ref().map(|h| h.opaque()) == current_host { break; } let outer_host = match outer_host { Some(h) => h, None => return false, }; // TODO(emilio): if worth it, we could early return if // host doesn't have the exportparts attribute. hosts.push(host); host = outer_host; } } // Translate the part into the right scope. parts.iter().all(|part| { let mut part = part.clone(); for host in hosts.iter().rev() { part = match host.imported_part(&part) { Some(p) => p, None => return false, }; } element.is_part(&part) }) } fn matches_host( element: &E, selector: Option<&Selector>, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue where E: Element, { let host = match context.shadow_host() { Some(h) => h, None => return KleeneValue::False, }; if host != element.opaque() { return KleeneValue::False; } let Some(selector) = selector else { return KleeneValue::True; }; context.nest(|context| { context.with_featureless(false, |context| { matches_complex_selector(selector.iter(), element, context, rightmost) }) }) } fn matches_slotted( element: &E, selector: &Selector, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue where E: Element, { // are never flattened tree slottables. if element.is_html_slot_element() { return KleeneValue::False; } context.nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost)) } fn matches_rare_attribute_selector( element: &E, attr_sel: &AttrSelectorWithOptionalNamespace, ) -> bool where E: Element, { let empty_string; let namespace = match attr_sel.namespace() { Some(ns) => ns, None => { empty_string = crate::parser::namespace_empty_string::(); NamespaceConstraint::Specific(&empty_string) }, }; element.attr_matches( &namespace, select_name(element, &attr_sel.local_name, &attr_sel.local_name_lower), &match attr_sel.operation { ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists, ParsedAttrSelectorOperation::WithValue { operator, case_sensitivity, ref value, } => AttrSelectorOperation::WithValue { operator, case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), value, }, }, ) } /// There are relatively few selectors in a given compound that may match a featureless element. /// Instead of adding a check to every selector that may not match, we handle it here in an out of /// line path. pub(crate) fn compound_matches_featureless_host( iter: &mut SelectorIter, scope_matches_featureless_host: bool, ) -> MatchesFeaturelessHost { let mut matches = MatchesFeaturelessHost::Only; for component in iter { match component { Component::Scope | Component::ImplicitScope if scope_matches_featureless_host => {}, // :host only matches featureless elements. Component::Host(..) => {}, // Pseudo-elements are allowed to match as well. Component::PseudoElement(..) => {}, // We allow logical pseudo-classes, but we'll fail matching of the inner selectors if // necessary. Component::Is(ref l) | Component::Where(ref l) => { let mut any_yes = false; let mut any_no = false; for selector in l.slice() { match selector.matches_featureless_host(scope_matches_featureless_host) { MatchesFeaturelessHost::Never => { any_no = true; }, MatchesFeaturelessHost::Yes => { any_yes = true; any_no = true; }, MatchesFeaturelessHost::Only => { any_yes = true; }, } } if !any_yes { return MatchesFeaturelessHost::Never; } if any_no { // Potentially downgrade since we might match non-featureless elements too. matches = MatchesFeaturelessHost::Yes; } }, Component::Negation(ref l) => { // For now preserving behavior, see // https://github.com/w3c/csswg-drafts/issues/10179 for existing resolutions that // tweak this behavior. for selector in l.slice() { if selector.matches_featureless_host(scope_matches_featureless_host) != MatchesFeaturelessHost::Only { return MatchesFeaturelessHost::Never; } } }, // Other components don't match the host scope. _ => return MatchesFeaturelessHost::Never, } } matches } /// Determines whether the given element matches the given compound selector. #[inline] fn matches_compound_selector( selector_iter: &mut SelectorIter, element: &E, context: &mut MatchingContext, rightmost: SubjectOrPseudoElement, ) -> KleeneValue where E: Element, { if context.featureless() && compound_matches_featureless_host( &mut selector_iter.clone(), /* scope_matches_featureless_host = */ true, ) == MatchesFeaturelessHost::Never { return KleeneValue::False; } let quirks_data = if context.quirks_mode() == QuirksMode::Quirks { Some(selector_iter.clone()) } else { None }; let mut local_context = LocalMatchingContext { shared: context, rightmost, quirks_data, }; KleeneValue::any_false(selector_iter, |simple| { matches_simple_selector(simple, element, &mut local_context) }) } /// Determines whether the given element matches the given single selector. fn matches_simple_selector( selector: &Component, element: &E, context: &mut LocalMatchingContext, ) -> KleeneValue where E: Element, { debug_assert!(context.shared.is_nested() || !context.shared.in_negation()); let rightmost = context.rightmost; KleeneValue::from(match *selector { Component::ID(ref id) => { element.has_id(id, context.shared.classes_and_ids_case_sensitivity()) }, Component::Class(ref class) => { element.has_class(class, context.shared.classes_and_ids_case_sensitivity()) }, Component::LocalName(ref local_name) => matches_local_name(element, local_name), Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, } => element.has_attr_in_no_namespace(select_name(element, local_name, local_name_lower)), Component::AttributeInNoNamespace { ref local_name, ref value, operator, case_sensitivity, } => element.attr_matches( &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::()), local_name, &AttrSelectorOperation::WithValue { operator, case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), value, }, ), Component::AttributeOther(ref attr_sel) => { matches_rare_attribute_selector(element, attr_sel) }, Component::Part(ref parts) => matches_part(element, parts, &mut context.shared), Component::Slotted(ref selector) => { return matches_slotted(element, selector, &mut context.shared, rightmost); }, Component::PseudoElement(ref pseudo) => { element.match_pseudo_element(pseudo, context.shared) }, Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => true, Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => { element.has_namespace(&url.borrow()) }, Component::ExplicitNoNamespace => { let ns = crate::parser::namespace_empty_string::(); element.has_namespace(&ns.borrow()) }, Component::NonTSPseudoClass(ref pc) => { if let Some(ref iter) = context.quirks_data { if pc.is_active_or_hover() && !element.is_link() && hover_and_active_quirk_applies(iter, context.shared, context.rightmost) { return KleeneValue::False; } } element.match_non_ts_pseudo_class(pc, &mut context.shared) }, Component::Root => element.is_root(), Component::Empty => { if context.shared.needs_selector_flags() { element.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); } element.is_empty() }, Component::Host(ref selector) => { return matches_host(element, selector.as_ref(), &mut context.shared, rightmost); }, Component::ParentSelector => match context.shared.scope_element { Some(ref scope_element) => element.opaque() == *scope_element, None => element.is_root(), }, Component::Scope | Component::ImplicitScope => { let matching_for_invalidation = context.shared.matching_for_invalidation_comparison(); if context.shared.matching_for_revalidation() || matching_for_invalidation.is_some() { let may_return_unknown = matching_for_invalidation.unwrap_or(false); return if may_return_unknown { KleeneValue::Unknown } else { KleeneValue::from(!context.shared.in_negation()) }; } match context.shared.scope_element { Some(ref scope_element) => element.opaque() == *scope_element, None => element.is_root(), } }, Component::Nth(ref nth_data) => { return matches_generic_nth_child(element, context.shared, nth_data, &[], rightmost); }, Component::NthOf(ref nth_of_data) => { return context.shared.nest(|context| { matches_generic_nth_child( element, context, nth_of_data.nth_data(), nth_of_data.selectors(), rightmost, ) }) }, Component::Is(ref list) | Component::Where(ref list) => { return context.shared.nest(|context| { matches_complex_selector_list(list.slice(), element, context, rightmost) }) }, Component::Negation(ref list) => { return context.shared.nest_for_negation(|context| { !matches_complex_selector_list(list.slice(), element, context, rightmost) }) }, Component::Has(ref relative_selectors) => { return match_relative_selectors( relative_selectors, element, context.shared, rightmost, ); }, Component::Combinator(_) => unsafe { debug_unreachable!("Shouldn't try to selector-match combinators") }, Component::RelativeSelectorAnchor => { let anchor = context.shared.relative_selector_anchor(); // We may match inner relative selectors, in which case we want to always match. anchor.map_or(true, |a| a == element.opaque()) }, Component::Invalid(..) => false, }) } #[inline(always)] pub fn select_name<'a, E: Element, T: PartialEq>( element: &E, local_name: &'a T, local_name_lower: &'a T, ) -> &'a T { if local_name == local_name_lower || element.is_html_element_in_html_document() { local_name_lower } else { local_name } } #[inline(always)] pub fn to_unconditional_case_sensitivity<'a, E: Element>( parsed: ParsedCaseSensitivity, element: &E, ) -> CaseSensitivity { match parsed { ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => { CaseSensitivity::CaseSensitive }, ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive, ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { if element.is_html_element_in_html_document() { CaseSensitivity::AsciiCaseInsensitive } else { CaseSensitivity::CaseSensitive } }, } } fn matches_generic_nth_child( element: &E, context: &mut MatchingContext, nth_data: &NthSelectorData, selectors: &[Selector], rightmost: SubjectOrPseudoElement, ) -> KleeneValue where E: Element, { if element.ignores_nth_child_selectors() { return KleeneValue::False; } let has_selectors = !selectors.is_empty(); let selectors_match = !has_selectors || matches_complex_selector_list(selectors, element, context, rightmost).to_bool(true); if let Some(may_return_unknown) = context.matching_for_invalidation_comparison() { // Skip expensive indexing math in invalidation. return if selectors_match && may_return_unknown { KleeneValue::Unknown } else { KleeneValue::from(selectors_match && !context.in_negation()) }; } let NthSelectorData { ty, an_plus_b, .. } = *nth_data; let is_of_type = ty.is_of_type(); if ty.is_only() { debug_assert!( !has_selectors, ":only-child and :only-of-type cannot have a selector list!" ); return KleeneValue::from( matches_generic_nth_child( element, context, &NthSelectorData::first(is_of_type), selectors, rightmost, ) .to_bool(true) && matches_generic_nth_child( element, context, &NthSelectorData::last(is_of_type), selectors, rightmost, ) .to_bool(true), ); } let is_from_end = ty.is_from_end(); // It's useful to know whether this can only select the first/last element // child for optimization purposes, see the `HAS_EDGE_CHILD_SELECTOR` flag. let is_edge_child_selector = nth_data.is_simple_edge() && !has_selectors; if context.needs_selector_flags() { let mut flags = if is_edge_child_selector { ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR } else if is_from_end { ElementSelectorFlags::HAS_SLOW_SELECTOR } else { ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS }; flags |= if has_selectors { ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF } else { ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH }; element.apply_selector_flags(flags); } if !selectors_match { return KleeneValue::False; } // :first/last-child are rather trivial to match, don't bother with the // cache. if is_edge_child_selector { return if is_from_end { element.next_sibling_element() } else { element.prev_sibling_element() } .is_none() .into(); } // Lookup or compute the index. let index = if let Some(i) = context .nth_index_cache(is_of_type, is_from_end, selectors) .lookup(element.opaque()) { i } else { let i = nth_child_index( element, context, selectors, is_of_type, is_from_end, /* check_cache = */ true, rightmost, ); context .nth_index_cache(is_of_type, is_from_end, selectors) .insert(element.opaque(), i); i }; debug_assert_eq!( index, nth_child_index( element, context, selectors, is_of_type, is_from_end, /* check_cache = */ false, rightmost, ), "invalid cache" ); an_plus_b.matches_index(index).into() } #[inline] fn nth_child_index( element: &E, context: &mut MatchingContext, selectors: &[Selector], is_of_type: bool, is_from_end: bool, check_cache: bool, rightmost: SubjectOrPseudoElement, ) -> i32 where E: Element, { // The traversal mostly processes siblings left to right. So when we walk // siblings to the right when computing NthLast/NthLastOfType we're unlikely // to get cache hits along the way. As such, we take the hit of walking the // siblings to the left checking the cache in the is_from_end case (this // matches what Gecko does). The indices-from-the-left is handled during the // regular look further below. if check_cache && is_from_end && !context .nth_index_cache(is_of_type, is_from_end, selectors) .is_empty() { let mut index: i32 = 1; let mut curr = element.clone(); while let Some(e) = curr.prev_sibling_element() { curr = e; let matches = if is_of_type { element.is_same_type(&curr) } else if !selectors.is_empty() { matches_complex_selector_list(selectors, &curr, context, rightmost).to_bool(true) } else { true }; if !matches { continue; } if let Some(i) = context .nth_index_cache(is_of_type, is_from_end, selectors) .lookup(curr.opaque()) { return i - index; } index += 1; } } let mut index: i32 = 1; let mut curr = element.clone(); let next = |e: E| { if is_from_end { e.next_sibling_element() } else { e.prev_sibling_element() } }; while let Some(e) = next(curr) { curr = e; let matches = if is_of_type { element.is_same_type(&curr) } else if !selectors.is_empty() { matches_complex_selector_list(selectors, &curr, context, rightmost).to_bool(true) } else { true }; if !matches { continue; } // If we're computing indices from the left, check each element in the // cache. We handle the indices-from-the-right case at the top of this // function. if !is_from_end && check_cache { if let Some(i) = context .nth_index_cache(is_of_type, is_from_end, selectors) .lookup(curr.opaque()) { return i + index; } } index += 1; } index } ================================================ FILE: selectors/nth_index_cache.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::hash::Hash; use crate::{parser::Selector, tree::OpaqueElement, SelectorImpl}; use rustc_hash::FxHashMap; /// A cache to speed up matching of nth-index-like selectors. /// /// See [1] for some discussion around the design tradeoffs. /// /// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3 #[derive(Default)] pub struct NthIndexCache { nth: NthIndexCacheInner, nth_of_selectors: NthIndexOfSelectorsCaches, nth_last: NthIndexCacheInner, nth_last_of_selectors: NthIndexOfSelectorsCaches, nth_of_type: NthIndexCacheInner, nth_last_of_type: NthIndexCacheInner, } impl NthIndexCache { /// Gets the appropriate cache for the given parameters. pub fn get( &mut self, is_of_type: bool, is_from_end: bool, selectors: &[Selector], ) -> &mut NthIndexCacheInner { if is_of_type { return if is_from_end { &mut self.nth_last_of_type } else { &mut self.nth_of_type }; } if !selectors.is_empty() { return if is_from_end { self.nth_last_of_selectors.lookup(selectors) } else { self.nth_of_selectors.lookup(selectors) }; } if is_from_end { &mut self.nth_last } else { &mut self.nth } } } #[derive(Hash, Eq, PartialEq)] struct SelectorListCacheKey(usize); /// Use the selector list's pointer as the cache key impl SelectorListCacheKey { // :nth-child of selectors are reference-counted with `ThinArc`, so we know their pointers are stable. fn new(selectors: &[Selector]) -> Self { Self(selectors.as_ptr() as usize) } } /// Use a different map of cached indices per :nth-child's or :nth-last-child's selector list #[derive(Default)] pub struct NthIndexOfSelectorsCaches(FxHashMap); /// Get or insert a map of cached incides for the selector list of this /// particular :nth-child or :nth-last-child pseudoclass impl NthIndexOfSelectorsCaches { pub fn lookup( &mut self, selectors: &[Selector], ) -> &mut NthIndexCacheInner { self.0 .entry(SelectorListCacheKey::new(selectors)) .or_default() } } /// The concrete per-pseudo-class cache. #[derive(Default)] pub struct NthIndexCacheInner(FxHashMap); impl NthIndexCacheInner { /// Does a lookup for a given element in the cache. pub fn lookup(&mut self, el: OpaqueElement) -> Option { self.0.get(&el).copied() } /// Inserts an entry into the cache. pub fn insert(&mut self, element: OpaqueElement, index: i32) { self.0.insert(element, index); } /// Returns whether the cache is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } } ================================================ FILE: selectors/parser.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::attr::{AttrSelectorOperator, AttrSelectorWithOptionalNamespace}; use crate::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity}; use crate::bloom::BLOOM_HASH_MASK; use crate::builder::{ relative_selector_list_specificity_and_flags, selector_list_specificity_and_flags, SelectorBuilder, SelectorFlags, Specificity, SpecificityAndFlags, }; use crate::context::QuirksMode; use crate::sink::Push; use crate::visitor::SelectorListKind; pub use crate::visitor::SelectorVisitor; use bitflags::bitflags; use cssparser::match_ignore_ascii_case; use cssparser::parse_nth; use cssparser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind}; use cssparser::{CowRcStr, Delimiter, SourceLocation}; use cssparser::{Parser as CssParser, ToCss, Token}; use debug_unreachable::debug_unreachable; use precomputed_hash::PrecomputedHash; use servo_arc::{Arc, ArcUnionBorrow, ThinArc, ThinArcUnion, UniqueArc}; use smallvec::SmallVec; use std::borrow::{Borrow, Cow}; use std::fmt::{self, Debug}; use std::iter::Rev; use std::slice; #[cfg(feature = "to_shmem")] use to_shmem_derive::ToShmem; /// A trait that represents a pseudo-element. pub trait PseudoElement: Sized + ToCss { /// The `SelectorImpl` this pseudo-element is used for. type Impl: SelectorImpl; /// Whether the pseudo-element supports a given state selector to the right /// of it. fn accepts_state_pseudo_classes(&self) -> bool { false } /// Whether this pseudo-element is valid after a ::slotted(..) pseudo. fn valid_after_slotted(&self) -> bool { false } /// Whether this pseudo-element is valid when directly after a ::before/::after pseudo. fn valid_after_before_or_after(&self) -> bool { false } /// Whether this pseudo-element is element-backed. /// https://drafts.csswg.org/css-pseudo-4/#element-like fn parses_as_element_backed(&self) -> bool { false } /// Whether this pseudo-element is ::before or ::after pseudo element, /// which are treated specially when deciding what can come after them. /// https://drafts.csswg.org/css-pseudo-4/#generated-content fn is_before_or_after(&self) -> bool { false } /// The count we contribute to the specificity from this pseudo-element. fn specificity_count(&self) -> u32 { 1 } /// Whether this pseudo-element is in a pseudo-element tree (excluding the pseudo-element /// root). /// https://drafts.csswg.org/css-view-transitions-1/#pseudo-root fn is_in_pseudo_element_tree(&self) -> bool { false } } /// A trait that represents a pseudo-class. pub trait NonTSPseudoClass: Sized + ToCss { /// The `SelectorImpl` this pseudo-element is used for. type Impl: SelectorImpl; /// Whether this pseudo-class is :active or :hover. fn is_active_or_hover(&self) -> bool; /// Whether this pseudo-class belongs to: /// /// https://drafts.csswg.org/selectors-4/#useraction-pseudos fn is_user_action_state(&self) -> bool; fn visit(&self, _visitor: &mut V) -> bool where V: SelectorVisitor, { true } } /// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a /// Cow::Owned if `s` had to be converted into ASCII lowercase. fn to_ascii_lowercase(s: &str) -> Cow<'_, str> { if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') { let mut string = s.to_owned(); string[first_uppercase..].make_ascii_lowercase(); string.into() } else { s.into() } } bitflags! { /// Flags that indicate at which point of parsing a selector are we. #[derive(Copy, Clone)] struct SelectorParsingState: u16 { /// Whether we should avoid adding default namespaces to selectors that /// aren't type or universal selectors. const SKIP_DEFAULT_NAMESPACE = 1 << 0; /// Whether we've parsed a ::slotted() pseudo-element already. /// /// If so, then we can only parse a subset of pseudo-elements, and /// whatever comes after them if so. const AFTER_SLOTTED = 1 << 1; /// Whether we've parsed a ::part() or element-backed pseudo-element already. /// /// If so, then we can only parse a subset of pseudo-elements, and /// whatever comes after them if so. const AFTER_PART_LIKE = 1 << 2; /// Whether we've parsed a non-element-backed pseudo-element (as in, an /// `Impl::PseudoElement` thus not accounting for `::slotted` or /// `::part`) already. /// /// If so, then other pseudo-elements and most other selectors are /// disallowed. const AFTER_NON_ELEMENT_BACKED_PSEUDO = 1 << 3; /// Whether we've parsed a non-stateful pseudo-element (again, as-in /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are /// disallowed. If this flag is set, `AFTER_NON_ELEMENT_BACKED_PSEUDO` must be set /// as well. const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 4; // Whether we've parsed a generated pseudo-element (as in ::before, ::after). // If so then some other pseudo elements are disallowed (e.g. another generated pseudo) // while others allowed (e.g. ::marker). const AFTER_BEFORE_OR_AFTER_PSEUDO = 1 << 5; /// Whether we are after any of the pseudo-like things. const AFTER_PSEUDO = Self::AFTER_PART_LIKE.bits() | Self::AFTER_SLOTTED.bits() | Self::AFTER_NON_ELEMENT_BACKED_PSEUDO.bits() | Self::AFTER_BEFORE_OR_AFTER_PSEUDO.bits(); /// Whether we explicitly disallow combinators. const DISALLOW_COMBINATORS = 1 << 6; /// Whether we explicitly disallow pseudo-element-like things. const DISALLOW_PSEUDOS = 1 << 7; /// Whether we explicitly disallow relative selectors (i.e. `:has()`). const DISALLOW_RELATIVE_SELECTOR = 1 << 8; /// Whether we've parsed a pseudo-element which is in a pseudo-element tree (i.e. it is a /// descendant pseudo of a pseudo-element root). const IN_PSEUDO_ELEMENT_TREE = 1 << 9; } } impl SelectorParsingState { #[inline] fn allows_slotted(self) -> bool { !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) } #[inline] fn allows_part(self) -> bool { !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) } #[inline] fn allows_non_functional_pseudo_classes(self) -> bool { !self.intersects(Self::AFTER_SLOTTED | Self::AFTER_NON_STATEFUL_PSEUDO_ELEMENT) } #[inline] fn allows_tree_structural_pseudo_classes(self) -> bool { !self.intersects(Self::AFTER_PSEUDO) || self.intersects(Self::IN_PSEUDO_ELEMENT_TREE) } #[inline] fn allows_combinators(self) -> bool { !self.intersects(Self::DISALLOW_COMBINATORS) } #[inline] fn allows_only_child_pseudo_class_only(self) -> bool { self.intersects(Self::IN_PSEUDO_ELEMENT_TREE) } } pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>; #[derive(Clone, Debug, PartialEq)] pub enum SelectorParseErrorKind<'i> { NoQualifiedNameInAttributeSelector(Token<'i>), EmptySelector, DanglingCombinator, NonCompoundSelector, NonPseudoElementAfterSlotted, InvalidPseudoElementAfterSlotted, InvalidPseudoElementInsideWhere, InvalidState, UnexpectedTokenInAttributeSelector(Token<'i>), PseudoElementExpectedColon(Token<'i>), PseudoElementExpectedIdent(Token<'i>), NoIdentForPseudo(Token<'i>), UnsupportedPseudoClassOrElement(CowRcStr<'i>), UnexpectedIdent(CowRcStr<'i>), ExpectedNamespace(CowRcStr<'i>), ExpectedBarInAttr(Token<'i>), BadValueInAttr(Token<'i>), InvalidQualNameInAttr(Token<'i>), ExplicitNamespaceUnexpectedToken(Token<'i>), ClassNeedsIdent(Token<'i>), } macro_rules! with_all_bounds { ( [ $( $InSelector: tt )* ] [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ] ) => { /// This trait allows to define the parser implementation in regards /// of pseudo-classes/elements /// /// NB: We need Clone so that we can derive(Clone) on struct with that /// are parameterized on SelectorImpl. See /// pub trait SelectorImpl: Clone + Debug + Sized + 'static { type ExtraMatchingData<'a>: Sized + Default; type AttrValue: $($InSelector)*; type Identifier: $($InSelector)* + PrecomputedHash; type LocalName: $($InSelector)* + Borrow + PrecomputedHash; type NamespaceUrl: $($CommonBounds)* + Default + Borrow + PrecomputedHash; type NamespacePrefix: $($InSelector)* + Default; type BorrowedNamespaceUrl: ?Sized + Eq; type BorrowedLocalName: ?Sized + Eq; /// non tree-structural pseudo-classes /// (see: https://drafts.csswg.org/selectors/#structural-pseudos) type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass; /// pseudo-elements type PseudoElement: $($CommonBounds)* + PseudoElement; /// Whether attribute hashes should be collected for filtering /// purposes. fn should_collect_attr_hash(_name: &Self::LocalName) -> bool { false } } } } macro_rules! with_bounds { ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => { with_all_bounds! { [$($CommonBounds)* + $($FromStr)* + ToCss] [$($CommonBounds)*] [$($FromStr)*] } } } with_bounds! { [Clone + Eq] [for<'a> From<&'a str>] } pub trait Parser<'i> { type Impl: SelectorImpl; type Error: 'i + From>; /// Whether to parse the `::slotted()` pseudo-element. fn parse_slotted(&self) -> bool { false } /// Whether to parse the `::part()` pseudo-element. fn parse_part(&self) -> bool { false } /// Whether to parse the selector list of nth-child() or nth-last-child(). fn parse_nth_child_of(&self) -> bool { false } /// Whether to parse `:is` and `:where` pseudo-classes. fn parse_is_and_where(&self) -> bool { false } /// Whether to parse the :has pseudo-class. fn parse_has(&self) -> bool { false } /// Whether to parse the '&' delimiter as a parent selector. fn parse_parent_selector(&self) -> bool { false } /// Whether the given function name is an alias for the `:is()` function. fn is_is_alias(&self, _name: &str) -> bool { false } /// Whether to parse the `:host` pseudo-class. fn parse_host(&self) -> bool { false } /// Whether to allow forgiving selector-list parsing. fn allow_forgiving_selectors(&self) -> bool { true } /// This function can return an "Err" pseudo-element in order to support CSS2.1 /// pseudo-elements. fn parse_non_ts_pseudo_class( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result<::NonTSPseudoClass, ParseError<'i, Self::Error>> { Err( location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_non_ts_functional_pseudo_class<'t>( &self, name: CowRcStr<'i>, parser: &mut CssParser<'i, 't>, _after_part: bool, ) -> Result<::NonTSPseudoClass, ParseError<'i, Self::Error>> { Err( parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_pseudo_element( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result<::PseudoElement, ParseError<'i, Self::Error>> { Err( location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_functional_pseudo_element<'t>( &self, name: CowRcStr<'i>, arguments: &mut CssParser<'i, 't>, ) -> Result<::PseudoElement, ParseError<'i, Self::Error>> { Err( arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn default_namespace(&self) -> Option<::NamespaceUrl> { None } fn namespace_for_prefix( &self, _prefix: &::NamespacePrefix, ) -> Option<::NamespaceUrl> { None } } /// A selector list is a tagged pointer with either a single selector, or a ThinArc<()> of multiple /// selectors. #[derive(Clone, Eq, Debug, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct SelectorList( #[cfg_attr(feature = "to_shmem", shmem(field_bound))] ThinArcUnion, (), Selector>, ); impl SelectorList { /// See Arc::mark_as_intentionally_leaked pub fn mark_as_intentionally_leaked(&self) { if let ArcUnionBorrow::Second(ref list) = self.0.borrow() { list.with_arc(|list| list.mark_as_intentionally_leaked()) } self.slice() .iter() .for_each(|s| s.mark_as_intentionally_leaked()) } pub fn from_one(selector: Selector) -> Self { #[cfg(debug_assertions)] let selector_repr = unsafe { *(&selector as *const _ as *const usize) }; let list = Self(ThinArcUnion::from_first(selector.into_data())); #[cfg(debug_assertions)] debug_assert_eq!( selector_repr, unsafe { *(&list as *const _ as *const usize) }, "We rely on the same bit representation for the single selector variant" ); list } pub fn from_iter(mut iter: impl ExactSizeIterator>) -> Self { if iter.len() == 1 { Self::from_one(iter.next().unwrap()) } else { Self(ThinArcUnion::from_second(ThinArc::from_header_and_iter( (), iter, ))) } } #[inline] pub fn slice(&self) -> &[Selector] { match self.0.borrow() { ArcUnionBorrow::First(..) => { // SAFETY: see from_one. let selector: &Selector = unsafe { std::mem::transmute(self) }; std::slice::from_ref(selector) }, ArcUnionBorrow::Second(list) => list.get().slice(), } } #[inline] pub fn len(&self) -> usize { match self.0.borrow() { ArcUnionBorrow::First(..) => 1, ArcUnionBorrow::Second(list) => list.len(), } } /// Returns the address on the heap of the ThinArc for memory reporting. pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { match self.0.borrow() { ArcUnionBorrow::First(s) => s.with_arc(|a| a.heap_ptr()), ArcUnionBorrow::Second(s) => s.with_arc(|a| a.heap_ptr()), } } } /// Uniquely identify a selector based on its components, which is behind ThinArc and /// is therefore stable. #[derive(Clone, Copy, Hash, Eq, PartialEq)] pub struct SelectorKey(usize); impl SelectorKey { /// Create a new key based on the given selector. pub fn new(selector: &Selector) -> Self { Self(selector.0.slice().as_ptr() as usize) } } /// Whether or not we're using forgiving parsing mode #[derive(PartialEq)] enum ForgivingParsing { /// Discard the entire selector list upon encountering any invalid selector. /// This is the default behavior for almost all of CSS. No, /// Ignore invalid selectors, potentially creating an empty selector list. /// /// This is the error recovery mode of :is() and :where() Yes, } /// Flag indicating if we're parsing relative selectors. #[derive(Copy, Clone, PartialEq)] pub enum ParseRelative { /// Expect selectors to start with a combinator, assuming descendant combinator if not present. ForHas, /// Allow selectors to start with a combinator, prepending a parent selector if so. Do nothing /// otherwise ForNesting, /// Allow selectors to start with a combinator, prepending a scope selector if so. Do nothing /// otherwise ForScope, /// Treat as parse error if any selector begins with a combinator. No, } impl SelectorList { /// Returns a selector list with a single `:scope` selector (with specificity) pub fn scope() -> Self { Self::from_one(Selector::scope()) } /// Returns a selector list with a single implicit `:scope` selector (no specificity) pub fn implicit_scope() -> Self { Self::from_one(Selector::implicit_scope()) } /// Parse a comma-separated list of Selectors. /// /// /// Return the Selectors or Err if there is an invalid selector. pub fn parse<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, parse_relative: ParseRelative, ) -> Result> where P: Parser<'i, Impl = Impl>, { Self::parse_with_state( parser, input, SelectorParsingState::empty(), ForgivingParsing::No, parse_relative, ) } /// Same as `parse`, but disallow parsing of pseudo-elements. pub fn parse_disallow_pseudo<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, parse_relative: ParseRelative, ) -> Result> where P: Parser<'i, Impl = Impl>, { Self::parse_with_state( parser, input, SelectorParsingState::DISALLOW_PSEUDOS, ForgivingParsing::No, parse_relative, ) } pub fn parse_forgiving<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, parse_relative: ParseRelative, ) -> Result> where P: Parser<'i, Impl = Impl>, { Self::parse_with_state( parser, input, SelectorParsingState::empty(), ForgivingParsing::Yes, parse_relative, ) } #[inline] fn parse_with_state<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, recovery: ForgivingParsing, parse_relative: ParseRelative, ) -> Result> where P: Parser<'i, Impl = Impl>, { let mut values = SmallVec::<[_; 4]>::new(); let forgiving = recovery == ForgivingParsing::Yes && parser.allow_forgiving_selectors(); loop { let selector = input.parse_until_before(Delimiter::Comma, |input| { let start = input.position(); let mut selector = parse_selector(parser, input, state, parse_relative); if forgiving && (selector.is_err() || input.expect_exhausted().is_err()) { input.expect_no_error_token()?; selector = Ok(Selector::new_invalid(input.slice_from(start))); } selector })?; values.push(selector); match input.next() { Ok(&Token::Comma) => {}, Ok(_) => unreachable!(), Err(_) => break, } } Ok(Self::from_iter(values.into_iter())) } /// Replaces the parent selector in all the items of the selector list. pub fn replace_parent_selector(&self, parent: &SelectorList) -> Self { Self::from_iter( self.slice() .iter() .map(|selector| selector.replace_parent_selector(parent)), ) } /// Creates a SelectorList from a Vec of selectors. Used in tests. #[allow(dead_code)] pub(crate) fn from_vec(v: Vec>) -> Self { SelectorList::from_iter(v.into_iter()) } } /// Parses one compound selector suitable for nested stuff like :-moz-any, etc. fn parse_inner_compound_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { parse_selector( parser, input, state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS, ParseRelative::No, ) } /// Ancestor hashes for the bloom filter. We precompute these and store them /// inline with selectors to optimize cache performance during matching. /// This matters a lot. /// /// We use 4 hashes, which is copied from Gecko, who copied it from WebKit. /// Note that increasing the number of hashes here will adversely affect the /// cache hit when fast-rejecting long lists of Rules with inline hashes. /// /// Because the bloom filter only uses the bottom 24 bits of the hash, we pack /// the fourth hash into the upper bits of the first three hashes in order to /// shrink Rule (whose size matters a lot). This scheme minimizes the runtime /// overhead of the packing for the first three hashes (we just need to mask /// off the upper bits) at the expense of making the fourth somewhat more /// complicated to assemble, because we often bail out before checking all the /// hashes. #[derive(Clone, Debug, Eq, PartialEq)] pub struct AncestorHashes { pub packed_hashes: [u32; 3], } pub(crate) fn collect_selector_hashes<'a, Impl: SelectorImpl, Iter>( iter: Iter, quirks_mode: QuirksMode, hashes: &mut [u32; 4], len: &mut usize, create_inner_iterator: fn(&'a Selector) -> Iter, ) -> bool where Iter: Iterator>, { for component in iter { let hash = match *component { Component::LocalName(LocalName { ref name, ref lower_name, }) => { // Only insert the local-name into the filter if it's all // lowercase. Otherwise we would need to test both hashes, and // our data structures aren't really set up for that. if name != lower_name { continue; } name.precomputed_hash() }, Component::DefaultNamespace(ref url) | Component::Namespace(_, ref url) => { url.precomputed_hash() }, // In quirks mode, class and id selectors should match // case-insensitively, so just avoid inserting them into the filter. Component::ID(ref id) if quirks_mode != QuirksMode::Quirks => id.precomputed_hash(), Component::Class(ref class) if quirks_mode != QuirksMode::Quirks => { class.precomputed_hash() }, Component::AttributeInNoNamespace { ref local_name, .. } if Impl::should_collect_attr_hash(local_name) => { // AttributeInNoNamespace is only used when local_name == // local_name_lower. local_name.precomputed_hash() }, Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, .. } => { // Only insert the local-name into the filter if it's all // lowercase. Otherwise we would need to test both hashes, and // our data structures aren't really set up for that. if local_name != local_name_lower || !Impl::should_collect_attr_hash(local_name) { continue; } local_name.precomputed_hash() }, Component::AttributeOther(ref selector) => { if selector.local_name != selector.local_name_lower || !Impl::should_collect_attr_hash(&selector.local_name) { continue; } selector.local_name.precomputed_hash() }, Component::Is(ref list) | Component::Where(ref list) => { // :where and :is OR their selectors, so we can't put any hash // in the filter if there's more than one selector, as that'd // exclude elements that may match one of the other selectors. let slice = list.slice(); if slice.len() == 1 && !collect_selector_hashes( create_inner_iterator(&slice[0]), quirks_mode, hashes, len, create_inner_iterator, ) { return false; } continue; }, _ => continue, }; hashes[*len] = hash & BLOOM_HASH_MASK; *len += 1; if *len == hashes.len() { return false; } } true } fn collect_ancestor_hashes( iter: SelectorIter, quirks_mode: QuirksMode, hashes: &mut [u32; 4], len: &mut usize, ) { collect_selector_hashes(AncestorIter::new(iter), quirks_mode, hashes, len, |s| { AncestorIter(s.iter()) }); } impl AncestorHashes { pub fn new(selector: &Selector, quirks_mode: QuirksMode) -> Self { // Compute ancestor hashes for the bloom filter. let mut hashes = [0u32; 4]; let mut len = 0; collect_ancestor_hashes(selector.iter(), quirks_mode, &mut hashes, &mut len); debug_assert!(len <= 4); // Now, pack the fourth hash (if it exists) into the upper byte of each of // the other three hashes. if len == 4 { let fourth = hashes[3]; hashes[0] |= (fourth & 0x000000ff) << 24; hashes[1] |= (fourth & 0x0000ff00) << 16; hashes[2] |= (fourth & 0x00ff0000) << 8; } AncestorHashes { packed_hashes: [hashes[0], hashes[1], hashes[2]], } } /// Returns the fourth hash, reassembled from parts. pub fn fourth_hash(&self) -> u32 { ((self.packed_hashes[0] & 0xff000000) >> 24) | ((self.packed_hashes[1] & 0xff000000) >> 16) | ((self.packed_hashes[2] & 0xff000000) >> 8) } } #[inline] pub fn namespace_empty_string() -> Impl::NamespaceUrl { // Rust type’s default, not default namespace Impl::NamespaceUrl::default() } pub(super) type SelectorData = ThinArc>; /// Whether a selector may match a featureless host element, and whether it may match other /// elements. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MatchesFeaturelessHost { /// The selector may match a featureless host, but also a non-featureless element. Yes, /// The selector is guaranteed to never match a non-featureless host element. Only, /// The selector never matches a featureless host. Never, } impl MatchesFeaturelessHost { /// Whether we may match. #[inline] pub fn may_match(self) -> bool { return !matches!(self, Self::Never); } } /// A Selector stores a sequence of simple selectors and combinators. The /// iterator classes allow callers to iterate at either the raw sequence level or /// at the level of sequences of simple selectors separated by combinators. Most /// callers want the higher-level iterator. /// /// We store compound selectors internally right-to-left (in matching order). /// Additionally, we invert the order of top-level compound selectors so that /// each one matches left-to-right. This is because matching namespace, local name, /// id, and class are all relatively cheap, whereas matching pseudo-classes might /// be expensive (depending on the pseudo-class). Since authors tend to put the /// pseudo-classes on the right, it's faster to start matching on the left. /// /// This reordering doesn't change the semantics of selector matching, and we /// handle it in to_css to make it invisible to serialization. #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] #[repr(transparent)] pub struct Selector( #[cfg_attr(feature = "to_shmem", shmem(field_bound))] SelectorData, ); impl Selector { /// See Arc::mark_as_intentionally_leaked pub fn mark_as_intentionally_leaked(&self) { self.0.mark_as_intentionally_leaked() } fn scope() -> Self { Self(ThinArc::from_header_and_iter( SpecificityAndFlags { specificity: Specificity::single_class_like().into(), flags: SelectorFlags::HAS_SCOPE, }, std::iter::once(Component::Scope), )) } /// An implicit scope selector, much like :where(:scope). fn implicit_scope() -> Self { Self(ThinArc::from_header_and_iter( SpecificityAndFlags { specificity: 0, flags: SelectorFlags::HAS_SCOPE, }, std::iter::once(Component::ImplicitScope), )) } #[inline] pub fn specificity(&self) -> u32 { self.0.header.specificity } #[inline] pub(crate) fn flags(&self) -> SelectorFlags { self.0.header.flags } #[inline] pub fn has_pseudo_element(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_PSEUDO) } #[inline] pub fn has_parent_selector(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_PARENT) } #[inline] pub fn has_scope_selector(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_SCOPE) } #[inline] pub fn is_slotted(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_SLOTTED) } #[inline] pub fn is_part(&self) -> bool { self.flags().intersects(SelectorFlags::HAS_PART) } #[inline] pub fn parts(&self) -> Option<&[Impl::Identifier]> { if !self.is_part() { return None; } let mut iter = self.iter(); if self.has_pseudo_element() { // Skip the pseudo-element. for _ in &mut iter {} let combinator = iter.next_sequence()?; debug_assert_eq!(combinator, Combinator::PseudoElement); } for component in iter { if let Component::Part(ref part) = *component { return Some(part); } } debug_assert!(false, "is_part() lied somehow?"); None } #[inline] pub fn pseudo_element(&self) -> Option<&Impl::PseudoElement> { if !self.has_pseudo_element() { return None; } for component in self.iter() { if let Component::PseudoElement(ref pseudo) = *component { return Some(pseudo); } } debug_assert!(false, "has_pseudo_element lied!"); None } #[inline] pub fn pseudo_elements(&self) -> SmallVec<[&Impl::PseudoElement; 3]> { let mut pseudos = SmallVec::new(); if !self.has_pseudo_element() { return pseudos; } let mut iter = self.iter(); loop { for component in &mut iter { if let Component::PseudoElement(ref pseudo) = *component { pseudos.push(pseudo); } } match iter.next_sequence() { Some(Combinator::PseudoElement) => {}, _ => break, } } debug_assert!(!pseudos.is_empty(), "has_pseudo_element lied!"); pseudos } /// Whether this selector (pseudo-element part excluded) matches every element. /// /// Used for "pre-computed" pseudo-elements in components/style/stylist.rs #[inline] pub fn is_universal(&self) -> bool { self.iter_raw_match_order().all(|c| { matches!( *c, Component::ExplicitUniversalType | Component::ExplicitAnyNamespace | Component::Combinator(Combinator::PseudoElement) | Component::PseudoElement(..) ) }) } /// Whether this selector may match a featureless shadow host, with no combinators to the /// left, and optionally has a pseudo-element to the right. #[inline] pub fn matches_featureless_host( &self, scope_matches_featureless_host: bool, ) -> MatchesFeaturelessHost { let flags = self.flags(); if !flags.intersects(SelectorFlags::HAS_HOST | SelectorFlags::HAS_SCOPE) { return MatchesFeaturelessHost::Never; } let mut iter = self.iter(); if flags.intersects(SelectorFlags::HAS_PSEUDO) { for _ in &mut iter { // Skip over pseudo-elements } match iter.next_sequence() { Some(c) if c.is_pseudo_element() => {}, _ => { debug_assert!(false, "Pseudo selector without pseudo combinator?"); return MatchesFeaturelessHost::Never; }, } } let compound_matches = crate::matching::compound_matches_featureless_host( &mut iter, scope_matches_featureless_host, ); if iter.next_sequence().is_some() { return MatchesFeaturelessHost::Never; } return compound_matches; } /// Returns an iterator over this selector in matching order (right-to-left). /// When a combinator is reached, the iterator will return None, and /// next_sequence() may be called to continue to the next sequence. #[inline] pub fn iter(&self) -> SelectorIter<'_, Impl> { SelectorIter { iter: self.iter_raw_match_order(), next_combinator: None, } } /// Same as `iter()`, but skips `RelativeSelectorAnchor` and its associated combinator. #[inline] pub fn iter_skip_relative_selector_anchor(&self) -> SelectorIter<'_, Impl> { if cfg!(debug_assertions) { let mut selector_iter = self.iter_raw_parse_order_from(0); assert!( matches!( selector_iter.next().unwrap(), Component::RelativeSelectorAnchor ), "Relative selector does not start with RelativeSelectorAnchor" ); assert!( selector_iter.next().unwrap().is_combinator(), "Relative combinator does not exist" ); } SelectorIter { iter: self.0.slice()[..self.len() - 2].iter(), next_combinator: None, } } /// Returns an iterator over this selector in matching order (right-to-left), /// skipping the rightmost |offset| Components. #[inline] pub fn iter_from(&self, offset: usize) -> SelectorIter<'_, Impl> { let iter = self.0.slice()[offset..].iter(); SelectorIter { iter, next_combinator: None, } } /// Returns the combinator at index `index` (zero-indexed from the right), /// or panics if the component is not a combinator. #[inline] pub fn combinator_at_match_order(&self, index: usize) -> Combinator { match self.0.slice()[index] { Component::Combinator(c) => c, ref other => panic!( "Not a combinator: {:?}, {:?}, index: {}", other, self, index ), } } /// Returns an iterator over the entire sequence of simple selectors and /// combinators, in matching order (from right to left). #[inline] pub fn iter_raw_match_order(&self) -> slice::Iter<'_, Component> { self.0.slice().iter() } /// Returns the combinator at index `index` (zero-indexed from the left), /// or panics if the component is not a combinator. #[inline] pub fn combinator_at_parse_order(&self, index: usize) -> Combinator { match self.0.slice()[self.len() - index - 1] { Component::Combinator(c) => c, ref other => panic!( "Not a combinator: {:?}, {:?}, index: {}", other, self, index ), } } /// Returns an iterator over the sequence of simple selectors and /// combinators, in parse order (from left to right), starting from /// `offset`. #[inline] pub fn iter_raw_parse_order_from( &self, offset: usize, ) -> Rev>> { self.0.slice()[..self.len() - offset].iter().rev() } /// Creates a Selector from a vec of Components, specified in parse order. Used in tests. #[allow(dead_code)] pub(crate) fn from_vec( vec: Vec>, specificity: u32, flags: SelectorFlags, ) -> Self { let mut builder = SelectorBuilder::default(); for component in vec.into_iter() { if let Some(combinator) = component.as_combinator() { builder.push_combinator(combinator); } else { builder.push_simple_selector(component); } } let spec = SpecificityAndFlags { specificity, flags }; Selector(builder.build_with_specificity_and_flags(spec, ParseRelative::No)) } #[inline] fn into_data(self) -> SelectorData { self.0 } pub fn replace_parent_selector(&self, parent: &SelectorList) -> Self { let parent_specificity_and_flags = selector_list_specificity_and_flags( parent.slice().iter(), /* for_nesting_parent = */ true, ); let mut specificity = Specificity::from(self.specificity()); let mut flags = self.flags() - SelectorFlags::HAS_PARENT; let forbidden_flags = SelectorFlags::forbidden_for_nesting(); fn replace_parent_on_selector_list( orig: &[Selector], parent: &SelectorList, specificity: &mut Specificity, flags: &mut SelectorFlags, propagate_specificity: bool, forbidden_flags: SelectorFlags, ) -> Option> { if !orig.iter().any(|s| s.has_parent_selector()) { return None; } let result = SelectorList::from_iter(orig.iter().map(|s| s.replace_parent_selector(parent))); let result_specificity_and_flags = selector_list_specificity_and_flags( result.slice().iter(), /* for_nesting_parent = */ false, ); if propagate_specificity { *specificity += Specificity::from( result_specificity_and_flags.specificity - selector_list_specificity_and_flags( orig.iter(), /* for_nesting_parent = */ false, ) .specificity, ); } flags.insert(result_specificity_and_flags.flags - forbidden_flags); Some(result) } fn replace_parent_on_relative_selector_list( orig: &[RelativeSelector], parent: &SelectorList, specificity: &mut Specificity, flags: &mut SelectorFlags, forbidden_flags: SelectorFlags, ) -> Vec> { let mut any = false; let result = orig .iter() .map(|s| { if !s.selector.has_parent_selector() { return s.clone(); } any = true; RelativeSelector { match_hint: s.match_hint, selector: s.selector.replace_parent_selector(parent), } }) .collect(); if !any { return result; } let result_specificity_and_flags = relative_selector_list_specificity_and_flags( &result, /* for_nesting_parent = */ false, ); flags.insert(result_specificity_and_flags.flags - forbidden_flags); *specificity += Specificity::from( result_specificity_and_flags.specificity - relative_selector_list_specificity_and_flags( orig, /* for_nesting_parent = */ false, ) .specificity, ); result } fn replace_parent_on_selector( orig: &Selector, parent: &SelectorList, specificity: &mut Specificity, flags: &mut SelectorFlags, forbidden_flags: SelectorFlags, ) -> Selector { let new_selector = orig.replace_parent_selector(parent); *specificity += Specificity::from(new_selector.specificity() - orig.specificity()); flags.insert(new_selector.flags() - forbidden_flags); new_selector } if !self.has_parent_selector() { return self.clone(); } let iter = self.iter_raw_match_order().map(|component| { use self::Component::*; match *component { LocalName(..) | ID(..) | Class(..) | AttributeInNoNamespaceExists { .. } | AttributeInNoNamespace { .. } | AttributeOther(..) | ExplicitUniversalType | ExplicitAnyNamespace | ExplicitNoNamespace | DefaultNamespace(..) | Namespace(..) | Root | Empty | Scope | ImplicitScope | Nth(..) | NonTSPseudoClass(..) | PseudoElement(..) | Combinator(..) | Host(None) | Part(..) | Invalid(..) | RelativeSelectorAnchor => component.clone(), ParentSelector => { specificity += Specificity::from(parent_specificity_and_flags.specificity); flags.insert(parent_specificity_and_flags.flags - forbidden_flags); Is(parent.clone()) }, Negation(ref selectors) => { Negation( replace_parent_on_selector_list( selectors.slice(), parent, &mut specificity, &mut flags, /* propagate_specificity = */ true, forbidden_flags, ) .unwrap_or_else(|| selectors.clone()), ) }, Is(ref selectors) => { Is(replace_parent_on_selector_list( selectors.slice(), parent, &mut specificity, &mut flags, /* propagate_specificity = */ true, forbidden_flags, ) .unwrap_or_else(|| selectors.clone())) }, Where(ref selectors) => { Where( replace_parent_on_selector_list( selectors.slice(), parent, &mut specificity, &mut flags, /* propagate_specificity = */ false, forbidden_flags, ) .unwrap_or_else(|| selectors.clone()), ) }, Has(ref selectors) => Has(replace_parent_on_relative_selector_list( selectors, parent, &mut specificity, &mut flags, forbidden_flags, ) .into_boxed_slice()), Host(Some(ref selector)) => Host(Some(replace_parent_on_selector( selector, parent, &mut specificity, &mut flags, forbidden_flags, ))), NthOf(ref data) => { let selectors = replace_parent_on_selector_list( data.selectors(), parent, &mut specificity, &mut flags, /* propagate_specificity = */ true, forbidden_flags, ); NthOf(match selectors { Some(s) => { NthOfSelectorData::new(data.nth_data(), s.slice().iter().cloned()) }, None => data.clone(), }) }, Slotted(ref selector) => Slotted(replace_parent_on_selector( selector, parent, &mut specificity, &mut flags, forbidden_flags, )), } }); let mut items = UniqueArc::from_header_and_iter(Default::default(), iter); *items.header_mut() = SpecificityAndFlags { specificity: specificity.into(), flags, }; Selector(items.shareable()) } /// Returns count of simple selectors and combinators in the Selector. #[inline] pub fn len(&self) -> usize { self.0.len() } /// Returns the address on the heap of the ThinArc for memory reporting. pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { self.0.heap_ptr() } /// Traverse selector components inside `self`. /// /// Implementations of this method should call `SelectorVisitor` methods /// or other impls of `Visit` as appropriate based on the fields of `Self`. /// /// A return value of `false` indicates terminating the traversal. /// It should be propagated with an early return. /// On the contrary, `true` indicates that all fields of `self` have been traversed: /// /// ```rust,ignore /// if !visitor.visit_simple_selector(&self.some_simple_selector) { /// return false; /// } /// if !self.some_component.visit(visitor) { /// return false; /// } /// true /// ``` pub fn visit(&self, visitor: &mut V) -> bool where V: SelectorVisitor, { let mut current = self.iter(); let mut combinator = None; loop { if !visitor.visit_complex_selector(combinator) { return false; } for selector in &mut current { if !selector.visit(visitor) { return false; } } combinator = current.next_sequence(); if combinator.is_none() { break; } } true } /// Parse a selector, without any pseudo-element. #[inline] pub fn parse<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, ) -> Result> where P: Parser<'i, Impl = Impl>, { parse_selector( parser, input, SelectorParsingState::empty(), ParseRelative::No, ) } pub fn new_invalid(s: &str) -> Self { fn check_for_parent(input: &mut CssParser, has_parent: &mut bool) { while let Ok(t) = input.next() { match *t { Token::Function(_) | Token::ParenthesisBlock | Token::CurlyBracketBlock | Token::SquareBracketBlock => { let _ = input.parse_nested_block( |i| -> Result<(), ParseError<'_, BasicParseError>> { check_for_parent(i, has_parent); Ok(()) }, ); }, Token::Delim('&') => { *has_parent = true; }, _ => {}, } if *has_parent { break; } } } let mut has_parent = false; { let mut parser = cssparser::ParserInput::new(s); let mut parser = CssParser::new(&mut parser); check_for_parent(&mut parser, &mut has_parent); } Self(ThinArc::from_header_and_iter( SpecificityAndFlags { specificity: 0, flags: if has_parent { SelectorFlags::HAS_PARENT } else { SelectorFlags::empty() }, }, std::iter::once(Component::Invalid(Arc::new(String::from(s.trim())))), )) } /// Is the compound starting at the offset the subject compound, or referring to its pseudo-element? pub fn is_rightmost(&self, offset: usize) -> bool { // There can really be only one pseudo-element, and it's not really valid for anything else to // follow it. offset == 0 || matches!( self.combinator_at_match_order(offset - 1), Combinator::PseudoElement ) } } #[derive(Clone)] pub struct SelectorIter<'a, Impl: 'a + SelectorImpl> { iter: slice::Iter<'a, Component>, next_combinator: Option, } impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> { /// Prepares this iterator to point to the next sequence to the left, /// returning the combinator if the sequence was found. #[inline] pub fn next_sequence(&mut self) -> Option { self.next_combinator.take() } #[inline] pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool { let first = match self.next() { Some(c) => c, // Note that this is the common path that we keep inline: the // pseudo-element not having anything to its right. None => return true, }; self.matches_for_stateless_pseudo_element_internal(first) } #[inline(never)] fn matches_for_stateless_pseudo_element_internal(&mut self, first: &Component) -> bool { if !first.matches_for_stateless_pseudo_element() { return false; } for component in self { // The only other parser-allowed Components in this sequence are // state pseudo-classes, or one of the other things that can contain // them. if !component.matches_for_stateless_pseudo_element() { return false; } } true } /// Returns remaining count of the simple selectors and combinators in the Selector. #[inline] pub fn selector_length(&self) -> usize { self.iter.len() } } impl<'a, Impl: SelectorImpl> Iterator for SelectorIter<'a, Impl> { type Item = &'a Component; #[inline] fn next(&mut self) -> Option { debug_assert!( self.next_combinator.is_none(), "You should call next_sequence!" ); match *self.iter.next()? { Component::Combinator(c) => { self.next_combinator = Some(c); None }, ref x => Some(x), } } } impl<'a, Impl: SelectorImpl> fmt::Debug for SelectorIter<'a, Impl> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let iter = self.iter.clone().rev(); for component in iter { component.to_css(f)? } Ok(()) } } /// An iterator over all combinators in a selector. Does not traverse selectors within psuedoclasses. struct CombinatorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); impl<'a, Impl: 'a + SelectorImpl> CombinatorIter<'a, Impl> { fn new(inner: SelectorIter<'a, Impl>) -> Self { let mut result = CombinatorIter(inner); result.consume_non_combinators(); result } fn consume_non_combinators(&mut self) { while self.0.next().is_some() {} } } impl<'a, Impl: SelectorImpl> Iterator for CombinatorIter<'a, Impl> { type Item = Combinator; fn next(&mut self) -> Option { let result = self.0.next_sequence(); self.consume_non_combinators(); result } } /// An iterator over all simple selectors belonging to ancestors. struct AncestorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); impl<'a, Impl: 'a + SelectorImpl> AncestorIter<'a, Impl> { /// Creates an AncestorIter. The passed-in iterator is assumed to point to /// the beginning of the child sequence, which will be skipped. fn new(inner: SelectorIter<'a, Impl>) -> Self { let mut result = AncestorIter(inner); result.skip_until_ancestor(); result } /// Skips a sequence of simple selectors and all subsequent sequences until /// a non-pseudo-element ancestor combinator is reached. fn skip_until_ancestor(&mut self) { loop { while self.0.next().is_some() {} // If this is ever changed to stop at the "pseudo-element" // combinator, we will need to fix the way we compute hashes for // revalidation selectors. if self.0.next_sequence().map_or(true, |x| { matches!(x, Combinator::Child | Combinator::Descendant) }) { break; } } } } impl<'a, Impl: SelectorImpl> Iterator for AncestorIter<'a, Impl> { type Item = &'a Component; fn next(&mut self) -> Option { // Grab the next simple selector in the sequence if available. let next = self.0.next(); if next.is_some() { return next; } // See if there are more sequences. If so, skip any non-ancestor sequences. if let Some(combinator) = self.0.next_sequence() { if !matches!(combinator, Combinator::Child | Combinator::Descendant) { self.skip_until_ancestor(); } } self.0.next() } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum Combinator { Child, // > Descendant, // space NextSibling, // + LaterSibling, // ~ /// A dummy combinator we use to the left of pseudo-elements. /// /// It serializes as the empty string, and acts effectively as a child /// combinator in most cases. If we ever actually start using a child /// combinator for this, we will need to fix up the way hashes are computed /// for revalidation selectors. PseudoElement, /// Another combinator used for ::slotted(), which represent the jump from /// a node to its assigned slot. SlotAssignment, /// Another combinator used for `::part()`, which represents the jump from /// the part to the containing shadow host. Part, } impl Combinator { /// Returns true if this combinator is a child or descendant combinator. #[inline] pub fn is_ancestor(&self) -> bool { matches!( *self, Combinator::Child | Combinator::Descendant | Combinator::PseudoElement | Combinator::SlotAssignment ) } /// Returns true if this combinator is a pseudo-element combinator. #[inline] pub fn is_pseudo_element(&self) -> bool { matches!(*self, Combinator::PseudoElement) } /// Returns true if this combinator is a next- or later-sibling combinator. #[inline] pub fn is_sibling(&self) -> bool { matches!(*self, Combinator::NextSibling | Combinator::LaterSibling) } } /// An enum for the different types of :nth- pseudoclasses #[derive(Copy, Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub enum NthType { Child, LastChild, OnlyChild, OfType, LastOfType, OnlyOfType, } impl NthType { pub fn is_only(self) -> bool { self == Self::OnlyChild || self == Self::OnlyOfType } pub fn is_of_type(self) -> bool { self == Self::OfType || self == Self::LastOfType || self == Self::OnlyOfType } pub fn is_from_end(self) -> bool { self == Self::LastChild || self == Self::LastOfType } } /// The properties that comprise an An+B syntax #[derive(Copy, Clone, Eq, PartialEq, Debug)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct AnPlusB(pub i32, pub i32); impl AnPlusB { #[inline] pub fn matches_index(&self, i: i32) -> bool { // Is there a non-negative integer n such that An+B=i? match i.checked_sub(self.1) { None => false, Some(an) => match an.checked_div(self.0) { Some(n) => n >= 0 && self.0 * n == an, None /* a == 0 */ => an == 0, }, } } } impl ToCss for AnPlusB { /// Serialize (part of the CSS Syntax spec). /// #[inline] fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { match (self.0, self.1) { (0, 0) => dest.write_char('0'), (1, 0) => dest.write_char('n'), (-1, 0) => dest.write_str("-n"), (_, 0) => write!(dest, "{}n", self.0), (0, _) => write!(dest, "{}", self.1), (1, _) => write!(dest, "n{:+}", self.1), (-1, _) => write!(dest, "-n{:+}", self.1), (_, _) => write!(dest, "{}n{:+}", self.0, self.1), } } } /// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g., /// nth-child(An+B)). /// https://www.w3.org/TR/selectors-3/#nth-child-pseudo #[derive(Copy, Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct NthSelectorData { pub ty: NthType, pub is_function: bool, pub an_plus_b: AnPlusB, } impl NthSelectorData { /// Returns selector data for :only-{child,of-type} #[inline] pub const fn only(of_type: bool) -> Self { Self { ty: if of_type { NthType::OnlyOfType } else { NthType::OnlyChild }, is_function: false, an_plus_b: AnPlusB(0, 1), } } /// Returns selector data for :first-{child,of-type} #[inline] pub const fn first(of_type: bool) -> Self { Self { ty: if of_type { NthType::OfType } else { NthType::Child }, is_function: false, an_plus_b: AnPlusB(0, 1), } } /// Returns selector data for :last-{child,of-type} #[inline] pub const fn last(of_type: bool) -> Self { Self { ty: if of_type { NthType::LastOfType } else { NthType::LastChild }, is_function: false, an_plus_b: AnPlusB(0, 1), } } /// Returns true if this is an edge selector that is not `:*-of-type`` #[inline] pub fn is_simple_edge(&self) -> bool { self.an_plus_b.0 == 0 && self.an_plus_b.1 == 1 && !self.ty.is_of_type() && !self.ty.is_only() } /// Writes the beginning of the selector. #[inline] fn write_start(&self, dest: &mut W) -> fmt::Result { dest.write_str(match self.ty { NthType::Child if self.is_function => ":nth-child(", NthType::Child => ":first-child", NthType::LastChild if self.is_function => ":nth-last-child(", NthType::LastChild => ":last-child", NthType::OfType if self.is_function => ":nth-of-type(", NthType::OfType => ":first-of-type", NthType::LastOfType if self.is_function => ":nth-last-of-type(", NthType::LastOfType => ":last-of-type", NthType::OnlyChild => ":only-child", NthType::OnlyOfType => ":only-of-type", }) } #[inline] fn write_affine(&self, dest: &mut W) -> fmt::Result { self.an_plus_b.to_css(dest) } } /// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g., /// nth-child(An+B [of S]?)). /// https://www.w3.org/TR/selectors-4/#nth-child-pseudo #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct NthOfSelectorData( #[cfg_attr(feature = "to_shmem", shmem(field_bound))] ThinArc>, ); impl NthOfSelectorData { /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S]) #[inline] pub fn new(nth_data: &NthSelectorData, selectors: I) -> Self where I: Iterator> + ExactSizeIterator, { Self(ThinArc::from_header_and_iter(*nth_data, selectors)) } /// Returns the An+B part of the selector #[inline] pub fn nth_data(&self) -> &NthSelectorData { &self.0.header } /// Returns the selector list part of the selector #[inline] pub fn selectors(&self) -> &[Selector] { self.0.slice() } } /// Flag indicating where a given relative selector's match would be contained. #[derive(Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] pub enum RelativeSelectorMatchHint { /// Within this element's subtree. InSubtree, /// Within this element's direct children. InChild, /// This element's next sibling. InNextSibling, /// Within this element's next sibling's subtree. InNextSiblingSubtree, /// Within this element's subsequent siblings. InSibling, /// Across this element's subsequent siblings and their subtrees. InSiblingSubtree, } impl RelativeSelectorMatchHint { /// Create a new relative selector match hint based on its composition. pub fn new( relative_combinator: Combinator, has_child_or_descendants: bool, has_adjacent_or_next_siblings: bool, ) -> Self { match relative_combinator { Combinator::Descendant => RelativeSelectorMatchHint::InSubtree, Combinator::Child => { if !has_child_or_descendants { RelativeSelectorMatchHint::InChild } else { // Technically, for any composition that consists of child combinators only, // the search space is depth-constrained, but it's probably not worth optimizing for. RelativeSelectorMatchHint::InSubtree } }, Combinator::NextSibling => { if !has_child_or_descendants && !has_adjacent_or_next_siblings { RelativeSelectorMatchHint::InNextSibling } else if !has_child_or_descendants && has_adjacent_or_next_siblings { RelativeSelectorMatchHint::InSibling } else if has_child_or_descendants && !has_adjacent_or_next_siblings { // Match won't cross multiple siblings. RelativeSelectorMatchHint::InNextSiblingSubtree } else { RelativeSelectorMatchHint::InSiblingSubtree } }, Combinator::LaterSibling => { if !has_child_or_descendants { RelativeSelectorMatchHint::InSibling } else { // Even if the match may not cross multiple siblings, we have to look until // we find a match anyway. RelativeSelectorMatchHint::InSiblingSubtree } }, Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { debug_assert!(false, "Unexpected relative combinator"); RelativeSelectorMatchHint::InSubtree }, } } /// Is the match traversal direction towards the descendant of this element (As opposed to siblings)? pub fn is_descendant_direction(&self) -> bool { matches!(*self, Self::InChild | Self::InSubtree) } /// Is the match traversal terminated at the next sibling? pub fn is_next_sibling(&self) -> bool { matches!(*self, Self::InNextSibling | Self::InNextSiblingSubtree) } /// Does the match involve matching the subtree? pub fn is_subtree(&self) -> bool { matches!( *self, Self::InSubtree | Self::InSiblingSubtree | Self::InNextSiblingSubtree ) } } /// Count of combinators in a given relative selector, not traversing selectors of pseudoclasses. #[derive(Clone, Copy)] pub struct RelativeSelectorCombinatorCount { relative_combinator: Combinator, pub child_or_descendants: usize, pub adjacent_or_next_siblings: usize, } impl RelativeSelectorCombinatorCount { /// Create a new relative selector combinator count from a given relative selector. pub fn new(relative_selector: &RelativeSelector) -> Self { let mut result = RelativeSelectorCombinatorCount { relative_combinator: relative_selector.selector.combinator_at_parse_order(1), child_or_descendants: 0, adjacent_or_next_siblings: 0, }; for combinator in CombinatorIter::new( relative_selector .selector .iter_skip_relative_selector_anchor(), ) { match combinator { Combinator::Descendant | Combinator::Child => { result.child_or_descendants += 1; }, Combinator::NextSibling | Combinator::LaterSibling => { result.adjacent_or_next_siblings += 1; }, Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { continue; }, }; } result } /// Get the match hint based on the current combinator count. pub fn get_match_hint(&self) -> RelativeSelectorMatchHint { RelativeSelectorMatchHint::new( self.relative_combinator, self.child_or_descendants != 0, self.adjacent_or_next_siblings != 0, ) } } /// Storage for a relative selector. #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct RelativeSelector { /// Match space constraining hint. pub match_hint: RelativeSelectorMatchHint, /// The selector. Guaranteed to contain `RelativeSelectorAnchor` and the relative combinator in parse order. #[cfg_attr(feature = "to_shmem", shmem(field_bound))] pub selector: Selector, } bitflags! { /// Composition of combinators in a given selector, not traversing selectors of pseudoclasses. #[derive(Clone, Debug, Eq, PartialEq)] struct CombinatorComposition: u8 { const DESCENDANTS = 1 << 0; const SIBLINGS = 1 << 1; } } impl CombinatorComposition { fn for_relative_selector(inner_selector: &Selector) -> Self { let mut result = CombinatorComposition::empty(); for combinator in CombinatorIter::new(inner_selector.iter_skip_relative_selector_anchor()) { match combinator { Combinator::Descendant | Combinator::Child => { result.insert(Self::DESCENDANTS); }, Combinator::NextSibling | Combinator::LaterSibling => { result.insert(Self::SIBLINGS); }, Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { continue; }, }; if result.is_all() { break; } } return result; } } impl RelativeSelector { fn from_selector_list(selector_list: SelectorList) -> Box<[Self]> { selector_list .slice() .iter() .map(|selector| { // It's more efficient to keep track of all this during the parse time, but that seems like a lot of special // case handling for what it's worth. if cfg!(debug_assertions) { let relative_selector_anchor = selector.iter_raw_parse_order_from(0).next(); debug_assert!( relative_selector_anchor.is_some(), "Relative selector is empty" ); debug_assert!( matches!( relative_selector_anchor.unwrap(), Component::RelativeSelectorAnchor ), "Relative selector anchor is missing" ); } // Leave a hint for narrowing down the search space when we're matching. let composition = CombinatorComposition::for_relative_selector(&selector); let match_hint = RelativeSelectorMatchHint::new( selector.combinator_at_parse_order(1), composition.intersects(CombinatorComposition::DESCENDANTS), composition.intersects(CombinatorComposition::SIBLINGS), ); RelativeSelector { match_hint, selector: selector.clone(), } }) .collect() } } /// A CSS simple selector or combinator. We store both in the same enum for /// optimal packing and cache performance, see [1]. /// /// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973 #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub enum Component { LocalName(LocalName), ID(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::Identifier), Class(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::Identifier), AttributeInNoNamespaceExists { #[cfg_attr(feature = "to_shmem", shmem(field_bound))] local_name: Impl::LocalName, local_name_lower: Impl::LocalName, }, // Used only when local_name is already lowercase. AttributeInNoNamespace { local_name: Impl::LocalName, operator: AttrSelectorOperator, #[cfg_attr(feature = "to_shmem", shmem(field_bound))] value: Impl::AttrValue, case_sensitivity: ParsedCaseSensitivity, }, // Use a Box in the less common cases with more data to keep size_of::() small. AttributeOther(Box>), ExplicitUniversalType, ExplicitAnyNamespace, ExplicitNoNamespace, DefaultNamespace(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::NamespaceUrl), Namespace( #[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::NamespacePrefix, #[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::NamespaceUrl, ), /// Pseudo-classes Negation(SelectorList), Root, Empty, Scope, /// :scope added implicitly into scoped rules (i.e. In `@scope`) not /// explicitly using `:scope` or `&` selectors. /// /// https://drafts.csswg.org/css-cascade-6/#scoped-rules /// /// Unlike the normal `:scope` selector, this does not add any specificity. /// See https://github.com/w3c/csswg-drafts/issues/10196 ImplicitScope, ParentSelector, Nth(NthSelectorData), NthOf(NthOfSelectorData), NonTSPseudoClass(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::NonTSPseudoClass), /// The ::slotted() pseudo-element: /// /// https://drafts.csswg.org/css-scoping/#slotted-pseudo /// /// The selector here is a compound selector, that is, no combinators. /// /// NOTE(emilio): This should support a list of selectors, but as of this /// writing no other browser does, and that allows them to put ::slotted() /// in the rule hash, so we do that too. /// /// See https://github.com/w3c/csswg-drafts/issues/2158 Slotted(Selector), /// The `::part` pseudo-element. /// https://drafts.csswg.org/css-shadow-parts/#part Part(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Box<[Impl::Identifier]>), /// The `:host` pseudo-class: /// /// https://drafts.csswg.org/css-scoping/#host-selector /// /// NOTE(emilio): This should support a list of selectors, but as of this /// writing no other browser does, and that allows them to put :host() /// in the rule hash, so we do that too. /// /// See https://github.com/w3c/csswg-drafts/issues/2158 Host(Option>), /// The `:where` pseudo-class. /// /// https://drafts.csswg.org/selectors/#zero-matches /// /// The inner argument is conceptually a SelectorList, but we move the /// selectors to the heap to keep Component small. Where(SelectorList), /// The `:is` pseudo-class. /// /// https://drafts.csswg.org/selectors/#matches-pseudo /// /// Same comment as above re. the argument. Is(SelectorList), /// The `:has` pseudo-class. /// /// https://drafts.csswg.org/selectors/#has-pseudo /// /// Same comment as above re. the argument. Has(Box<[RelativeSelector]>), /// An invalid selector inside :is() / :where(). Invalid(Arc), /// An implementation-dependent pseudo-element selector. PseudoElement(#[cfg_attr(feature = "to_shmem", shmem(field_bound))] Impl::PseudoElement), Combinator(Combinator), /// Used only for relative selectors, which starts with a combinator /// (With an implied descendant combinator if not specified). /// /// https://drafts.csswg.org/selectors-4/#typedef-relative-selector RelativeSelectorAnchor, } impl Component { /// Returns true if this is a combinator. #[inline] pub fn is_combinator(&self) -> bool { matches!(*self, Component::Combinator(_)) } /// Returns true if this is a :host() selector. #[inline] pub fn is_host(&self) -> bool { matches!(*self, Component::Host(..)) } /// Returns the value as a combinator if applicable, None otherwise. pub fn as_combinator(&self) -> Option { match *self { Component::Combinator(c) => Some(c), _ => None, } } /// Whether a given selector (to the right of a pseudo-element) should match for stateless /// pseudo-elements. Note that generally nothing matches for those, but since we have :not(), /// we still need to traverse nested selector lists. fn matches_for_stateless_pseudo_element(&self) -> bool { match *self { Component::Negation(ref selectors) => !selectors.slice().iter().all(|selector| { selector .iter_raw_match_order() .all(|c| c.matches_for_stateless_pseudo_element()) }), Component::Is(ref selectors) | Component::Where(ref selectors) => { selectors.slice().iter().any(|selector| { selector .iter_raw_match_order() .all(|c| c.matches_for_stateless_pseudo_element()) }) }, _ => false, } } pub fn visit(&self, visitor: &mut V) -> bool where V: SelectorVisitor, { use self::Component::*; if !visitor.visit_simple_selector(self) { return false; } match *self { Slotted(ref selector) => { if !selector.visit(visitor) { return false; } }, Host(Some(ref selector)) => { if !selector.visit(visitor) { return false; } }, AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, } => { if !visitor.visit_attribute_selector( &NamespaceConstraint::Specific(&namespace_empty_string::()), local_name, local_name_lower, ) { return false; } }, AttributeInNoNamespace { ref local_name, .. } => { if !visitor.visit_attribute_selector( &NamespaceConstraint::Specific(&namespace_empty_string::()), local_name, local_name, ) { return false; } }, AttributeOther(ref attr_selector) => { let empty_string; let namespace = match attr_selector.namespace() { Some(ns) => ns, None => { empty_string = crate::parser::namespace_empty_string::(); NamespaceConstraint::Specific(&empty_string) }, }; if !visitor.visit_attribute_selector( &namespace, &attr_selector.local_name, &attr_selector.local_name_lower, ) { return false; } }, NonTSPseudoClass(ref pseudo_class) => { if !pseudo_class.visit(visitor) { return false; } }, Negation(ref list) | Is(ref list) | Where(ref list) => { let list_kind = SelectorListKind::from_component(self); debug_assert!(!list_kind.is_empty()); if !visitor.visit_selector_list(list_kind, list.slice()) { return false; } }, NthOf(ref nth_of_data) => { if !visitor.visit_selector_list(SelectorListKind::NTH_OF, nth_of_data.selectors()) { return false; } }, Has(ref list) => { if !visitor.visit_relative_selector_list(list) { return false; } }, _ => {}, } true } // Returns true if this has any selector that requires an index calculation. e.g. // :nth-child, :first-child, etc. For nested selectors, return true only if the // indexed selector is in its subject compound. pub fn has_indexed_selector_in_subject(&self) -> bool { match *self { Component::NthOf(..) | Component::Nth(..) => return true, Component::Is(ref selectors) | Component::Where(ref selectors) | Component::Negation(ref selectors) => { // Check the subject compound. for selector in selectors.slice() { let mut iter = selector.iter(); while let Some(c) = iter.next() { if c.has_indexed_selector_in_subject() { return true; } } } }, _ => (), }; false } } #[derive(Clone, Eq, PartialEq)] #[cfg_attr(feature = "to_shmem", derive(ToShmem))] #[cfg_attr(feature = "to_shmem", shmem(no_bounds))] pub struct LocalName { #[cfg_attr(feature = "to_shmem", shmem(field_bound))] pub name: Impl::LocalName, pub lower_name: Impl::LocalName, } impl Debug for Selector { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("Selector(")?; self.to_css(f)?; write!( f, ", specificity = {:#x}, flags = {:?})", self.specificity(), self.flags() ) } } impl Debug for Component { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } } impl Debug for AttrSelectorWithOptionalNamespace { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } } impl Debug for LocalName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.to_css(f) } } fn serialize_selector_list<'a, Impl, I, W>(iter: I, dest: &mut W) -> fmt::Result where Impl: SelectorImpl, I: Iterator>, W: fmt::Write, { let mut first = true; for selector in iter { if !first { dest.write_str(", ")?; } first = false; selector.to_css(dest)?; } Ok(()) } impl ToCss for SelectorList { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { serialize_selector_list(self.slice().iter(), dest) } } impl ToCss for Selector { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { // Compound selectors invert the order of their contents, so we need to // undo that during serialization. // // This two-iterator strategy involves walking over the selector twice. // We could do something more clever, but selector serialization probably // isn't hot enough to justify it, and the stringification likely // dominates anyway. // // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), // which we need for |split|. So we split by combinators on a match-order // sequence and then reverse. let mut combinators = self .iter_raw_match_order() .rev() .filter_map(|x| x.as_combinator()); let compound_selectors = self .iter_raw_match_order() .as_slice() .split(|x| x.is_combinator()) .rev(); let mut combinators_exhausted = false; for compound in compound_selectors { debug_assert!(!combinators_exhausted); // https://drafts.csswg.org/cssom/#serializing-selectors let first_compound = match compound.first() { None => continue, Some(c) => c, }; if matches!( first_compound, Component::RelativeSelectorAnchor | Component::ImplicitScope ) { debug_assert!( compound.len() == 1, "RelativeSelectorAnchor/ImplicitScope should only be a simple selector" ); if let Some(c) = combinators.next() { c.to_css_relative(dest)?; } else { // Direct property declarations in `@scope` does not have // combinators, since its selector is `:implicit-scope`. debug_assert!( matches!(first_compound, Component::ImplicitScope), "Only implicit :scope may not have any combinator" ); } continue; } // 1. If there is only one simple selector in the compound selectors // which is a universal selector, append the result of // serializing the universal selector to s. // // Check if `!compound.empty()` first--this can happen if we have // something like `... > ::before`, because we store `>` and `::` // both as combinators internally. // // If we are in this case, after we have serialized the universal // selector, we skip Step 2 and continue with the algorithm. let (can_elide_namespace, first_non_namespace) = match compound[0] { Component::ExplicitAnyNamespace | Component::ExplicitNoNamespace | Component::Namespace(..) => (false, 1), Component::DefaultNamespace(..) => (true, 1), _ => (true, 0), }; let mut perform_step_2 = true; let next_combinator = combinators.next(); if first_non_namespace == compound.len() - 1 { match (next_combinator, &compound[first_non_namespace]) { // We have to be careful here, because if there is a // pseudo element "combinator" there isn't really just // the one simple selector. Technically this compound // selector contains the pseudo element selector as well // -- Combinator::PseudoElement, just like // Combinator::SlotAssignment, don't exist in the // spec. (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (), (_, &Component::ExplicitUniversalType) => { // Iterate over everything so we serialize the namespace // too. for simple in compound.iter() { simple.to_css(dest)?; } // Skip step 2, which is an "otherwise". perform_step_2 = false; }, _ => (), } } // 2. Otherwise, for each simple selector in the compound selectors // that is not a universal selector of which the namespace prefix // maps to a namespace that is not the default namespace // serialize the simple selector and append the result to s. // // See https://github.com/w3c/csswg-drafts/issues/1606, which is // proposing to change this to match up with the behavior asserted // in cssom/serialize-namespaced-type-selectors.html, which the // following code tries to match. if perform_step_2 { for simple in compound.iter() { if let Component::ExplicitUniversalType = *simple { // Can't have a namespace followed by a pseudo-element // selector followed by a universal selector in the same // compound selector, so we don't have to worry about the // real namespace being in a different `compound`. if can_elide_namespace { continue; } } simple.to_css(dest)?; } } // 3. If this is not the last part of the chain of the selector // append a single SPACE (U+0020), followed by the combinator // ">", "+", "~", ">>", "||", as appropriate, followed by another // single SPACE (U+0020) if the combinator was not whitespace, to // s. match next_combinator { Some(c) => c.to_css(dest)?, None => combinators_exhausted = true, }; // 4. If this is the last part of the chain of the selector and // there is a pseudo-element, append "::" followed by the name of // the pseudo-element, to s. // // (we handle this above) } Ok(()) } } impl Combinator { fn to_css_internal(&self, dest: &mut W, prefix_space: bool) -> fmt::Result where W: fmt::Write, { if matches!( *self, Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment ) { return Ok(()); } if prefix_space { dest.write_char(' ')?; } match *self { Combinator::Child => dest.write_str("> "), Combinator::Descendant => Ok(()), Combinator::NextSibling => dest.write_str("+ "), Combinator::LaterSibling => dest.write_str("~ "), Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => unsafe { debug_unreachable!("Already handled") }, } } fn to_css_relative(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { self.to_css_internal(dest, false) } } impl ToCss for Combinator { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { self.to_css_internal(dest, true) } } impl ToCss for Component { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { use self::Component::*; match *self { Combinator(ref c) => c.to_css(dest), Slotted(ref selector) => { dest.write_str("::slotted(")?; selector.to_css(dest)?; dest.write_char(')') }, Part(ref part_names) => { dest.write_str("::part(")?; for (i, name) in part_names.iter().enumerate() { if i != 0 { dest.write_char(' ')?; } name.to_css(dest)?; } dest.write_char(')') }, PseudoElement(ref p) => p.to_css(dest), ID(ref s) => { dest.write_char('#')?; s.to_css(dest) }, Class(ref s) => { dest.write_char('.')?; s.to_css(dest) }, LocalName(ref s) => s.to_css(dest), ExplicitUniversalType => dest.write_char('*'), DefaultNamespace(_) => Ok(()), ExplicitNoNamespace => dest.write_char('|'), ExplicitAnyNamespace => dest.write_str("*|"), Namespace(ref prefix, _) => { prefix.to_css(dest)?; dest.write_char('|') }, AttributeInNoNamespaceExists { ref local_name, .. } => { dest.write_char('[')?; local_name.to_css(dest)?; dest.write_char(']') }, AttributeInNoNamespace { ref local_name, operator, ref value, case_sensitivity, .. } => { dest.write_char('[')?; local_name.to_css(dest)?; operator.to_css(dest)?; value.to_css(dest)?; match case_sensitivity { ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { }, ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, } dest.write_char(']') }, AttributeOther(ref attr_selector) => attr_selector.to_css(dest), // Pseudo-classes Root => dest.write_str(":root"), Empty => dest.write_str(":empty"), Scope => dest.write_str(":scope"), ParentSelector => dest.write_char('&'), Host(ref selector) => { dest.write_str(":host")?; if let Some(ref selector) = *selector { dest.write_char('(')?; selector.to_css(dest)?; dest.write_char(')')?; } Ok(()) }, Nth(ref nth_data) => { nth_data.write_start(dest)?; if nth_data.is_function { nth_data.write_affine(dest)?; dest.write_char(')')?; } Ok(()) }, NthOf(ref nth_of_data) => { let nth_data = nth_of_data.nth_data(); nth_data.write_start(dest)?; debug_assert!( nth_data.is_function, "A selector must be a function to hold An+B notation" ); nth_data.write_affine(dest)?; debug_assert!( matches!(nth_data.ty, NthType::Child | NthType::LastChild), "Only :nth-child or :nth-last-child can be of a selector list" ); debug_assert!( !nth_of_data.selectors().is_empty(), "The selector list should not be empty" ); dest.write_str(" of ")?; serialize_selector_list(nth_of_data.selectors().iter(), dest)?; dest.write_char(')') }, Is(ref list) | Where(ref list) | Negation(ref list) => { match *self { Where(..) => dest.write_str(":where(")?, Is(..) => dest.write_str(":is(")?, Negation(..) => dest.write_str(":not(")?, _ => unreachable!(), } serialize_selector_list(list.slice().iter(), dest)?; dest.write_str(")") }, Has(ref list) => { dest.write_str(":has(")?; serialize_selector_list(list.iter().map(|rel| &rel.selector), dest)?; dest.write_str(")") }, NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), Invalid(ref css) => dest.write_str(css), RelativeSelectorAnchor | ImplicitScope => Ok(()), } } } impl ToCss for AttrSelectorWithOptionalNamespace { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { dest.write_char('[')?; match self.namespace { Some(NamespaceConstraint::Specific((ref prefix, _))) => { prefix.to_css(dest)?; dest.write_char('|')? }, Some(NamespaceConstraint::Any) => dest.write_str("*|")?, None => {}, } self.local_name.to_css(dest)?; match self.operation { ParsedAttrSelectorOperation::Exists => {}, ParsedAttrSelectorOperation::WithValue { operator, case_sensitivity, ref value, } => { operator.to_css(dest)?; value.to_css(dest)?; match case_sensitivity { ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { }, ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, } }, } dest.write_char(']') } } impl ToCss for LocalName { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { self.name.to_css(dest) } } /// Build up a Selector. /// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ; /// /// `Err` means invalid selector. fn parse_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, mut state: SelectorParsingState, parse_relative: ParseRelative, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { let mut builder = SelectorBuilder::default(); // Helps rewind less, but also simplifies dealing with relative combinators below. input.skip_whitespace(); if parse_relative != ParseRelative::No { let combinator = try_parse_combinator(input); match parse_relative { ParseRelative::ForHas => { builder.push_simple_selector(Component::RelativeSelectorAnchor); // Do we see a combinator? If so, push that. Otherwise, push a descendant // combinator. builder.push_combinator(combinator.unwrap_or(Combinator::Descendant)); }, ParseRelative::ForNesting | ParseRelative::ForScope => { if let Ok(combinator) = combinator { let selector = match parse_relative { ParseRelative::ForHas | ParseRelative::No => unreachable!(), ParseRelative::ForNesting => Component::ParentSelector, // See https://github.com/w3c/csswg-drafts/issues/10196 // Implicitly added `:scope` does not add specificity // for non-relative selectors, so do the same. ParseRelative::ForScope => Component::ImplicitScope, }; builder.push_simple_selector(selector); builder.push_combinator(combinator); } }, ParseRelative::No => unreachable!(), } } loop { // Parse a sequence of simple selectors. let empty = parse_compound_selector(parser, &mut state, input, &mut builder)?; if empty { return Err(input.new_custom_error(if builder.has_combinators() { SelectorParseErrorKind::DanglingCombinator } else { SelectorParseErrorKind::EmptySelector })); } if state.intersects(SelectorParsingState::AFTER_PSEUDO) { debug_assert!(state.intersects( SelectorParsingState::AFTER_NON_ELEMENT_BACKED_PSEUDO | SelectorParsingState::AFTER_BEFORE_OR_AFTER_PSEUDO | SelectorParsingState::AFTER_SLOTTED | SelectorParsingState::AFTER_PART_LIKE )); break; } let combinator = if let Ok(c) = try_parse_combinator(input) { c } else { break; }; if !state.allows_combinators() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } builder.push_combinator(combinator); } return Ok(Selector(builder.build(parse_relative))); } fn try_parse_combinator<'i, 't>(input: &mut CssParser<'i, 't>) -> Result { let mut any_whitespace = false; loop { let before_this_token = input.state(); match input.next_including_whitespace() { Err(_e) => return Err(()), Ok(&Token::WhiteSpace(_)) => any_whitespace = true, Ok(&Token::Delim('>')) => { return Ok(Combinator::Child); }, Ok(&Token::Delim('+')) => { return Ok(Combinator::NextSibling); }, Ok(&Token::Delim('~')) => { return Ok(Combinator::LaterSibling); }, Ok(_) => { input.reset(&before_this_token); if any_whitespace { return Ok(Combinator::Descendant); } else { return Err(()); } }, } } } /// * `Err(())`: Invalid selector, abort /// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed. /// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`) fn parse_type_selector<'i, 't, P, Impl, S>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, sink: &mut S, ) -> Result> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, S: Push>, { match parse_qualified_name(parser, input, /* in_attr_selector = */ false) { Err(ParseError { kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), .. }) | Ok(OptionalQName::None(_)) => Ok(false), Ok(OptionalQName::Some(namespace, local_name)) => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } match namespace { QNamePrefix::ImplicitAnyNamespace => {}, QNamePrefix::ImplicitDefaultNamespace(url) => { sink.push(Component::DefaultNamespace(url)) }, QNamePrefix::ExplicitNamespace(prefix, url) => { sink.push(match parser.default_namespace() { Some(ref default_url) if url == *default_url => { Component::DefaultNamespace(url) }, _ => Component::Namespace(prefix, url), }) }, QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), QNamePrefix::ExplicitAnyNamespace => { match parser.default_namespace() { // Element type selectors that have no namespace // component (no namespace separator) represent elements // without regard to the element's namespace (equivalent // to "*|") unless a default namespace has been declared // for namespaced selectors (e.g. in CSS, in the style // sheet). If a default namespace has been declared, // such selectors will represent only elements in the // default namespace. // -- Selectors § 6.1.1 // So we'll have this act the same as the // QNamePrefix::ImplicitAnyNamespace case. None => {}, Some(_) => sink.push(Component::ExplicitAnyNamespace), } }, QNamePrefix::ImplicitNoNamespace => { unreachable!() // Not returned with in_attr_selector = false }, } match local_name { Some(name) => sink.push(Component::LocalName(LocalName { lower_name: to_ascii_lowercase(&name).as_ref().into(), name: name.as_ref().into(), })), None => sink.push(Component::ExplicitUniversalType), } Ok(true) }, Err(e) => Err(e), } } #[derive(Debug)] enum SimpleSelectorParseResult { SimpleSelector(Component), PseudoElement(Impl::PseudoElement), SlottedPseudo(Selector), PartPseudo(Box<[Impl::Identifier]>), } #[derive(Debug)] enum QNamePrefix { ImplicitNoNamespace, // `foo` in attr selectors ImplicitAnyNamespace, // `foo` in type selectors, without a default ns ImplicitDefaultNamespace(Impl::NamespaceUrl), // `foo` in type selectors, with a default ns ExplicitNoNamespace, // `|foo` ExplicitAnyNamespace, // `*|foo` ExplicitNamespace(Impl::NamespacePrefix, Impl::NamespaceUrl), // `prefix|foo` } enum OptionalQName<'i, Impl: SelectorImpl> { Some(QNamePrefix, Option>), None(Token<'i>), } /// * `Err(())`: Invalid selector, abort /// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed, /// but the token is still returned. /// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector fn parse_qualified_name<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, in_attr_selector: bool, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { let default_namespace = |local_name| { let namespace = match parser.default_namespace() { Some(url) => QNamePrefix::ImplicitDefaultNamespace(url), None => QNamePrefix::ImplicitAnyNamespace, }; Ok(OptionalQName::Some(namespace, local_name)) }; let explicit_namespace = |input: &mut CssParser<'i, 't>, namespace| { let location = input.current_source_location(); match input.next_including_whitespace() { Ok(&Token::Delim('*')) if !in_attr_selector => Ok(OptionalQName::Some(namespace, None)), Ok(&Token::Ident(ref local_name)) => { Ok(OptionalQName::Some(namespace, Some(local_name.clone()))) }, Ok(t) if in_attr_selector => { let e = SelectorParseErrorKind::InvalidQualNameInAttr(t.clone()); Err(location.new_custom_error(e)) }, Ok(t) => Err(location.new_custom_error( SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t.clone()), )), Err(e) => Err(e.into()), } }; let start = input.state(); match input.next_including_whitespace() { Ok(Token::Ident(value)) => { let value = value.clone(); let after_ident = input.state(); match input.next_including_whitespace() { Ok(&Token::Delim('|')) => { let prefix = value.as_ref().into(); let result = parser.namespace_for_prefix(&prefix); let url = result.ok_or( after_ident .source_location() .new_custom_error(SelectorParseErrorKind::ExpectedNamespace(value)), )?; explicit_namespace(input, QNamePrefix::ExplicitNamespace(prefix, url)) }, _ => { input.reset(&after_ident); if in_attr_selector { Ok(OptionalQName::Some( QNamePrefix::ImplicitNoNamespace, Some(value), )) } else { default_namespace(Some(value)) } }, } }, Ok(Token::Delim('*')) => { let after_star = input.state(); match input.next_including_whitespace() { Ok(&Token::Delim('|')) => { explicit_namespace(input, QNamePrefix::ExplicitAnyNamespace) }, _ if !in_attr_selector => { input.reset(&after_star); default_namespace(None) }, result => { let t = result?; Err(after_star .source_location() .new_custom_error(SelectorParseErrorKind::ExpectedBarInAttr(t.clone()))) }, } }, Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitNoNamespace), Ok(t) => { let t = t.clone(); input.reset(&start); Ok(OptionalQName::None(t)) }, Err(e) => { input.reset(&start); Err(e.into()) }, } } fn parse_attribute_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { let namespace; let local_name; input.skip_whitespace(); match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? { OptionalQName::None(t) => { return Err(input.new_custom_error( SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t), )); }, OptionalQName::Some(_, None) => unreachable!(), OptionalQName::Some(ns, Some(ln)) => { local_name = ln; namespace = match ns { QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => None, QNamePrefix::ExplicitNamespace(prefix, url) => { Some(NamespaceConstraint::Specific((prefix, url))) }, QNamePrefix::ExplicitAnyNamespace => Some(NamespaceConstraint::Any), QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => { unreachable!() // Not returned with in_attr_selector = true }, } }, } let location = input.current_source_location(); let operator = match input.next() { // [foo] Err(_) => { let local_name_lower = to_ascii_lowercase(&local_name).as_ref().into(); let local_name = local_name.as_ref().into(); if let Some(namespace) = namespace { return Ok(Component::AttributeOther(Box::new( AttrSelectorWithOptionalNamespace { namespace: Some(namespace), local_name, local_name_lower, operation: ParsedAttrSelectorOperation::Exists, }, ))); } else { return Ok(Component::AttributeInNoNamespaceExists { local_name, local_name_lower, }); } }, // [foo=bar] Ok(&Token::Delim('=')) => AttrSelectorOperator::Equal, // [foo~=bar] Ok(&Token::IncludeMatch) => AttrSelectorOperator::Includes, // [foo|=bar] Ok(&Token::DashMatch) => AttrSelectorOperator::DashMatch, // [foo^=bar] Ok(&Token::PrefixMatch) => AttrSelectorOperator::Prefix, // [foo*=bar] Ok(&Token::SubstringMatch) => AttrSelectorOperator::Substring, // [foo$=bar] Ok(&Token::SuffixMatch) => AttrSelectorOperator::Suffix, Ok(t) => { return Err(location.new_custom_error( SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t.clone()), )); }, }; let value = match input.expect_ident_or_string() { Ok(t) => t.clone(), Err(BasicParseError { kind: BasicParseErrorKind::UnexpectedToken(t), location, }) => return Err(location.new_custom_error(SelectorParseErrorKind::BadValueInAttr(t))), Err(e) => return Err(e.into()), }; let attribute_flags = parse_attribute_flags(input)?; let value = value.as_ref().into(); let local_name_lower; let local_name_is_ascii_lowercase; let case_sensitivity; { let local_name_lower_cow = to_ascii_lowercase(&local_name); case_sensitivity = attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some()); local_name_lower = local_name_lower_cow.as_ref().into(); local_name_is_ascii_lowercase = matches!(local_name_lower_cow, Cow::Borrowed(..)); } let local_name = local_name.as_ref().into(); if namespace.is_some() || !local_name_is_ascii_lowercase { Ok(Component::AttributeOther(Box::new( AttrSelectorWithOptionalNamespace { namespace, local_name, local_name_lower, operation: ParsedAttrSelectorOperation::WithValue { operator, case_sensitivity, value, }, }, ))) } else { Ok(Component::AttributeInNoNamespace { local_name, operator, value, case_sensitivity, }) } } /// An attribute selector can have 's' or 'i' as flags, or no flags at all. enum AttributeFlags { // Matching should be case-sensitive ('s' flag). CaseSensitive, // Matching should be case-insensitive ('i' flag). AsciiCaseInsensitive, // No flags. Matching behavior depends on the name of the attribute. CaseSensitivityDependsOnName, } impl AttributeFlags { fn to_case_sensitivity( self, local_name_lower: &str, have_namespace: bool, ) -> ParsedCaseSensitivity { match self { AttributeFlags::CaseSensitive => ParsedCaseSensitivity::ExplicitCaseSensitive, AttributeFlags::AsciiCaseInsensitive => ParsedCaseSensitivity::AsciiCaseInsensitive, AttributeFlags::CaseSensitivityDependsOnName => { if !have_namespace && include!(concat!( env!("OUT_DIR"), "/ascii_case_insensitive_html_attributes.rs" )) .contains(local_name_lower) { ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument } else { ParsedCaseSensitivity::CaseSensitive } }, } } } fn parse_attribute_flags<'i, 't>( input: &mut CssParser<'i, 't>, ) -> Result> { let location = input.current_source_location(); let token = match input.next() { Ok(t) => t, Err(..) => { // Selectors spec says language-defined; HTML says it depends on the // exact attribute name. return Ok(AttributeFlags::CaseSensitivityDependsOnName); }, }; let ident = match *token { Token::Ident(ref i) => i, ref other => return Err(location.new_basic_unexpected_token_error(other.clone())), }; Ok(match_ignore_ascii_case! { ident, "i" => AttributeFlags::AsciiCaseInsensitive, "s" => AttributeFlags::CaseSensitive, _ => return Err(location.new_basic_unexpected_token_error(token.clone())), }) } /// Level 3: Parse **one** simple_selector. (Though we might insert a second /// implied "|*" type selector.) fn parse_negation<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { let list = SelectorList::parse_with_state( parser, input, state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS, ForgivingParsing::No, ParseRelative::No, )?; Ok(Component::Negation(list)) } /// simple_selector_sequence /// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]* /// | [ HASH | class | attrib | pseudo | negation ]+ /// /// `Err(())` means invalid selector. /// `Ok(true)` is an empty selector fn parse_compound_selector<'i, 't, P, Impl>( parser: &P, state: &mut SelectorParsingState, input: &mut CssParser<'i, 't>, builder: &mut SelectorBuilder, ) -> Result> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { input.skip_whitespace(); let mut empty = true; if parse_type_selector(parser, input, *state, builder)? { empty = false; } loop { let result = match parse_one_simple_selector(parser, input, *state)? { None => break, Some(result) => result, }; if empty { if let Some(url) = parser.default_namespace() { // If there was no explicit type selector, but there is a // default namespace, there is an implicit "|*" type // selector. Except for :host() or :not() / :is() / :where(), // where we ignore it. // // https://drafts.csswg.org/css-scoping/#host-element-in-tree: // // When considered within its own shadow trees, the shadow // host is featureless. Only the :host, :host(), and // :host-context() pseudo-classes are allowed to match it. // // https://drafts.csswg.org/selectors-4/#featureless: // // A featureless element does not match any selector at all, // except those it is explicitly defined to match. If a // given selector is allowed to match a featureless element, // it must do so while ignoring the default namespace. // // https://drafts.csswg.org/selectors-4/#matches // // Default namespace declarations do not affect the compound // selector representing the subject of any selector within // a :is() pseudo-class, unless that compound selector // contains an explicit universal selector or type selector. // // (Similar quotes for :where() / :not()) // let ignore_default_ns = state .intersects(SelectorParsingState::SKIP_DEFAULT_NAMESPACE) || matches!( result, SimpleSelectorParseResult::SimpleSelector(Component::Host(..)) ); if !ignore_default_ns { builder.push_simple_selector(Component::DefaultNamespace(url)); } } } empty = false; match result { SimpleSelectorParseResult::SimpleSelector(s) => { builder.push_simple_selector(s); }, SimpleSelectorParseResult::PartPseudo(part_names) => { state.insert(SelectorParsingState::AFTER_PART_LIKE); builder.push_combinator(Combinator::Part); builder.push_simple_selector(Component::Part(part_names)); }, SimpleSelectorParseResult::SlottedPseudo(selector) => { state.insert(SelectorParsingState::AFTER_SLOTTED); builder.push_combinator(Combinator::SlotAssignment); builder.push_simple_selector(Component::Slotted(selector)); }, SimpleSelectorParseResult::PseudoElement(p) => { if p.parses_as_element_backed() { state.insert(SelectorParsingState::AFTER_PART_LIKE); } else { state.insert(SelectorParsingState::AFTER_NON_ELEMENT_BACKED_PSEUDO); if p.is_before_or_after() { state.insert(SelectorParsingState::AFTER_BEFORE_OR_AFTER_PSEUDO); } } if !p.accepts_state_pseudo_classes() { state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT); } if p.is_in_pseudo_element_tree() { state.insert(SelectorParsingState::IN_PSEUDO_ELEMENT_TREE); } builder.push_combinator(Combinator::PseudoElement); builder.push_simple_selector(Component::PseudoElement(p)); }, } } Ok(empty) } fn parse_is_where<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, component: impl FnOnce(SelectorList) -> Component, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { debug_assert!(parser.parse_is_and_where()); // https://drafts.csswg.org/selectors/#matches-pseudo: // // Pseudo-elements cannot be represented by the matches-any // pseudo-class; they are not valid within :is(). // let inner = SelectorList::parse_with_state( parser, input, state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS, ForgivingParsing::Yes, ParseRelative::No, )?; Ok(component(inner)) } fn parse_has<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { debug_assert!(parser.parse_has()); if state.intersects( SelectorParsingState::DISALLOW_RELATIVE_SELECTOR | SelectorParsingState::AFTER_PSEUDO, ) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } // Nested `:has()` is disallowed, mark it as such. // Note: The spec defines ":has-allowed pseudo-element," but there's no // pseudo-element defined as such at the moment. // https://w3c.github.io/csswg-drafts/selectors-4/#has-allowed-pseudo-element let inner = SelectorList::parse_with_state( parser, input, state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_RELATIVE_SELECTOR, ForgivingParsing::No, ParseRelative::ForHas, )?; Ok(Component::Has(RelativeSelector::from_selector_list(inner))) } fn parse_functional_pseudo_class<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, name: CowRcStr<'i>, state: SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { match_ignore_ascii_case! { &name, "nth-child" => return parse_nth_pseudo_class(parser, input, state, NthType::Child), "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::OfType), "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, NthType::LastChild), "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::LastOfType), "is" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Is), "where" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Where), "has" if parser.parse_has() => return parse_has(parser, input, state), "host" => { if !state.allows_tree_structural_pseudo_classes() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input, state)?))); }, "not" => { return parse_negation(parser, input, state) }, _ => {} } if parser.parse_is_and_where() && parser.is_is_alias(&name) { return parse_is_where(parser, input, state, Component::Is); } if state.intersects( SelectorParsingState::AFTER_NON_ELEMENT_BACKED_PSEUDO | SelectorParsingState::AFTER_SLOTTED, ) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let after_part = state.intersects(SelectorParsingState::AFTER_PART_LIKE); P::parse_non_ts_functional_pseudo_class(parser, name, input, after_part) .map(Component::NonTSPseudoClass) } fn parse_nth_pseudo_class<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, ty: NthType, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { if !state.allows_tree_structural_pseudo_classes() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let (a, b) = parse_nth(input)?; let nth_data = NthSelectorData { ty, is_function: true, an_plus_b: AnPlusB(a, b), }; if !parser.parse_nth_child_of() || ty.is_of_type() { return Ok(Component::Nth(nth_data)); } // Try to parse "of ". if input.try_parse(|i| i.expect_ident_matching("of")).is_err() { return Ok(Component::Nth(nth_data)); } // Whitespace between "of" and the selector list is optional // https://github.com/w3c/csswg-drafts/issues/8285 let selectors = SelectorList::parse_with_state( parser, input, state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS, ForgivingParsing::No, ParseRelative::No, )?; Ok(Component::NthOf(NthOfSelectorData::new( &nth_data, selectors.slice().iter().cloned(), ))) } /// Returns whether the name corresponds to a CSS2 pseudo-element that /// can be specified with the single colon syntax (in addition to the /// double-colon syntax, which can be used for all pseudo-elements). pub fn is_css2_pseudo_element(name: &str) -> bool { // ** Do not add to this list! ** match_ignore_ascii_case! { name, "before" | "after" | "first-line" | "first-letter" => true, _ => false, } } /// Parse a simple selector other than a type selector. /// /// * `Err(())`: Invalid selector, abort /// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed. /// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element fn parse_one_simple_selector<'i, 't, P, Impl>( parser: &P, input: &mut CssParser<'i, 't>, state: SelectorParsingState, ) -> Result>, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { let start = input.state(); let token = match input.next_including_whitespace().map(|t| t.clone()) { Ok(t) => t, Err(..) => { input.reset(&start); return Ok(None); }, }; Ok(Some(match token { Token::IDHash(id) => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let id = Component::ID(id.as_ref().into()); SimpleSelectorParseResult::SimpleSelector(id) }, Token::Delim(delim) if delim == '.' || (delim == '&' && parser.parse_parent_selector()) => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let location = input.current_source_location(); SimpleSelectorParseResult::SimpleSelector(if delim == '&' { Component::ParentSelector } else { let class = match *input.next_including_whitespace()? { Token::Ident(ref class) => class, ref t => { let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone()); return Err(location.new_custom_error(e)); }, }; Component::Class(class.as_ref().into()) }) }, Token::SquareBracketBlock => { if state.intersects(SelectorParsingState::AFTER_PSEUDO) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?; SimpleSelectorParseResult::SimpleSelector(attr) }, Token::Colon => { let location = input.current_source_location(); let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() { Token::Colon => (false, input.next_including_whitespace()?.clone()), t => (true, t), }; let (name, is_functional) = match next_token { Token::Ident(name) => (name, false), Token::Function(name) => (name, true), t => { let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t); return Err(input.new_custom_error(e)); }, }; let is_pseudo_element = !is_single_colon || is_css2_pseudo_element(&name); if is_pseudo_element { // Pseudos after pseudo elements are not allowed in some cases: // - Some states will disallow pseudos, such as the interiors of // :has/:is/:where/:not (DISALLOW_PSEUDOS). // - Non-element backed pseudos do not allow other pseudos to follow (AFTER_NON_ELEMENT_BACKED_PSEUDO)... // - ... except ::before and ::after, which allow _some_ pseudos. if state.intersects(SelectorParsingState::DISALLOW_PSEUDOS) || (state.intersects(SelectorParsingState::AFTER_NON_ELEMENT_BACKED_PSEUDO) && !state.intersects(SelectorParsingState::AFTER_BEFORE_OR_AFTER_PSEUDO)) { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } let pseudo_element = if is_functional { if P::parse_part(parser) && name.eq_ignore_ascii_case("part") { if !state.allows_part() { return Err( input.new_custom_error(SelectorParseErrorKind::InvalidState) ); } let names = input.parse_nested_block(|input| { let mut result = Vec::with_capacity(1); result.push(input.expect_ident()?.as_ref().into()); while !input.is_exhausted() { result.push(input.expect_ident()?.as_ref().into()); } Ok(result.into_boxed_slice()) })?; return Ok(Some(SimpleSelectorParseResult::PartPseudo(names))); } if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") { if !state.allows_slotted() { return Err( input.new_custom_error(SelectorParseErrorKind::InvalidState) ); } let selector = input.parse_nested_block(|input| { parse_inner_compound_selector(parser, input, state) })?; return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector))); } input.parse_nested_block(|input| { P::parse_functional_pseudo_element(parser, name, input) })? } else { P::parse_pseudo_element(parser, location, name)? }; if state.intersects(SelectorParsingState::AFTER_BEFORE_OR_AFTER_PSEUDO) && !pseudo_element.valid_after_before_or_after() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } if state.intersects(SelectorParsingState::AFTER_SLOTTED) && !pseudo_element.valid_after_slotted() { return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); } SimpleSelectorParseResult::PseudoElement(pseudo_element) } else { let pseudo_class = if is_functional { input.parse_nested_block(|input| { parse_functional_pseudo_class(parser, input, name, state) })? } else { parse_simple_pseudo_class(parser, location, name, state)? }; SimpleSelectorParseResult::SimpleSelector(pseudo_class) } }, _ => { input.reset(&start); return Ok(None); }, })) } fn parse_simple_pseudo_class<'i, P, Impl>( parser: &P, location: SourceLocation, name: CowRcStr<'i>, state: SelectorParsingState, ) -> Result, ParseError<'i, P::Error>> where P: Parser<'i, Impl = Impl>, Impl: SelectorImpl, { if !state.allows_non_functional_pseudo_classes() { return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); } if state.allows_tree_structural_pseudo_classes() { // If a descendant pseudo of a pseudo-element root has no other siblings, then :only-child // matches that pseudo. Note that we don't accept other tree structural pseudo classes in // this case (to match other browsers). And the spec mentions only `:only-child` as well. // https://drafts.csswg.org/css-view-transitions-1/#pseudo-root if state.allows_only_child_pseudo_class_only() { if name.eq_ignore_ascii_case("only-child") { return Ok(Component::Nth(NthSelectorData::only( /* of_type = */ false, ))); } // Other non-functional pseudo classes are not allowed. // FIXME: Perhaps we can refactor this, e.g. distinguish tree-structural pseudo classes // from other non-ts pseudo classes. Otherwise, this special case looks weird. return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); } match_ignore_ascii_case! { &name, "first-child" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ false))), "last-child" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ false))), "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))), "root" => return Ok(Component::Root), "empty" => return Ok(Component::Empty), "scope" => return Ok(Component::Scope), "host" if P::parse_host(parser) => return Ok(Component::Host(None)), "first-of-type" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ true))), "last-of-type" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ true))), "only-of-type" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ true))), _ => {}, } } let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?; if state.intersects(SelectorParsingState::AFTER_NON_ELEMENT_BACKED_PSEUDO) && !pseudo_class.is_user_action_state() { return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); } Ok(Component::NonTSPseudoClass(pseudo_class)) } // NB: pub module in order to access the DummyParser #[cfg(test)] pub mod tests { use super::*; use crate::builder::SelectorFlags; use crate::parser; use cssparser::{serialize_identifier, Parser as CssParser, ParserInput, ToCss}; use std::collections::HashMap; use std::fmt; #[derive(Clone, Debug, Eq, PartialEq)] pub enum PseudoClass { Hover, Active, Lang(String), } #[derive(Clone, Debug, Eq, PartialEq)] pub enum PseudoElement { Before, After, Marker, DetailsContent, Highlight(String), } impl parser::PseudoElement for PseudoElement { type Impl = DummySelectorImpl; fn accepts_state_pseudo_classes(&self) -> bool { true } fn valid_after_slotted(&self) -> bool { true } fn valid_after_before_or_after(&self) -> bool { matches!(self, Self::Marker) } fn is_before_or_after(&self) -> bool { matches!(self, Self::Before | Self::After) } fn parses_as_element_backed(&self) -> bool { matches!(self, Self::DetailsContent) } } impl parser::NonTSPseudoClass for PseudoClass { type Impl = DummySelectorImpl; #[inline] fn is_active_or_hover(&self) -> bool { matches!(*self, PseudoClass::Active | PseudoClass::Hover) } #[inline] fn is_user_action_state(&self) -> bool { self.is_active_or_hover() } } impl ToCss for PseudoClass { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { match *self { PseudoClass::Hover => dest.write_str(":hover"), PseudoClass::Active => dest.write_str(":active"), PseudoClass::Lang(ref lang) => { dest.write_str(":lang(")?; serialize_identifier(lang, dest)?; dest.write_char(')') }, } } } impl ToCss for PseudoElement { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { match *self { PseudoElement::Before => dest.write_str("::before"), PseudoElement::After => dest.write_str("::after"), PseudoElement::Marker => dest.write_str("::marker"), PseudoElement::DetailsContent => dest.write_str("::details-content"), PseudoElement::Highlight(ref name) => { dest.write_str("::highlight(")?; serialize_identifier(&name, dest)?; dest.write_char(')') }, } } } #[derive(Clone, Debug, PartialEq)] pub struct DummySelectorImpl; #[derive(Default)] pub struct DummyParser { default_ns: Option, ns_prefixes: HashMap, } impl DummyParser { fn default_with_namespace(default_ns: DummyAtom) -> DummyParser { DummyParser { default_ns: Some(default_ns), ns_prefixes: Default::default(), } } } impl SelectorImpl for DummySelectorImpl { type ExtraMatchingData<'a> = std::marker::PhantomData<&'a ()>; type AttrValue = DummyAttrValue; type Identifier = DummyAtom; type LocalName = DummyAtom; type NamespaceUrl = DummyAtom; type NamespacePrefix = DummyAtom; type BorrowedLocalName = DummyAtom; type BorrowedNamespaceUrl = DummyAtom; type NonTSPseudoClass = PseudoClass; type PseudoElement = PseudoElement; } #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct DummyAttrValue(String); impl ToCss for DummyAttrValue { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { use std::fmt::Write; dest.write_char('"')?; write!(cssparser::CssStringWriter::new(dest), "{}", &self.0)?; dest.write_char('"') } } impl<'a> From<&'a str> for DummyAttrValue { fn from(string: &'a str) -> Self { Self(string.into()) } } #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct DummyAtom(String); impl ToCss for DummyAtom { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { serialize_identifier(&self.0, dest) } } impl From for DummyAtom { fn from(string: String) -> Self { DummyAtom(string) } } impl<'a> From<&'a str> for DummyAtom { fn from(string: &'a str) -> Self { DummyAtom(string.into()) } } impl PrecomputedHash for DummyAtom { fn precomputed_hash(&self) -> u32 { self.0.as_ptr() as u32 } } impl<'i> Parser<'i> for DummyParser { type Impl = DummySelectorImpl; type Error = SelectorParseErrorKind<'i>; fn parse_slotted(&self) -> bool { true } fn parse_nth_child_of(&self) -> bool { true } fn parse_is_and_where(&self) -> bool { true } fn parse_has(&self) -> bool { true } fn parse_parent_selector(&self) -> bool { true } fn parse_part(&self) -> bool { true } fn parse_host(&self) -> bool { true } fn parse_non_ts_pseudo_class( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result> { match_ignore_ascii_case! { &name, "hover" => return Ok(PseudoClass::Hover), "active" => return Ok(PseudoClass::Active), _ => {} } Err( location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_non_ts_functional_pseudo_class<'t>( &self, name: CowRcStr<'i>, parser: &mut CssParser<'i, 't>, after_part: bool, ) -> Result> { match_ignore_ascii_case! { &name, "lang" if !after_part => { let lang = parser.expect_ident_or_string()?.as_ref().to_owned(); return Ok(PseudoClass::Lang(lang)); }, _ => {} } Err( parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_pseudo_element( &self, location: SourceLocation, name: CowRcStr<'i>, ) -> Result> { match_ignore_ascii_case! { &name, "before" => return Ok(PseudoElement::Before), "after" => return Ok(PseudoElement::After), "marker" => return Ok(PseudoElement::Marker), "details-content" => return Ok(PseudoElement::DetailsContent), _ => {} } Err( location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn parse_functional_pseudo_element<'t>( &self, name: CowRcStr<'i>, parser: &mut CssParser<'i, 't>, ) -> Result> { match_ignore_ascii_case! { &name, "highlight" => return Ok(PseudoElement::Highlight(parser.expect_ident()?.as_ref().to_owned())), _ => {} } Err( parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( name, )), ) } fn default_namespace(&self) -> Option { self.default_ns.clone() } fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option { self.ns_prefixes.get(prefix).cloned() } } fn parse<'i>( input: &'i str, ) -> Result, SelectorParseError<'i>> { parse_relative(input, ParseRelative::No) } fn parse_relative<'i>( input: &'i str, parse_relative: ParseRelative, ) -> Result, SelectorParseError<'i>> { parse_ns_relative(input, &DummyParser::default(), parse_relative) } fn parse_expected<'i, 'a>( input: &'i str, expected: Option<&'a str>, ) -> Result, SelectorParseError<'i>> { parse_ns_expected(input, &DummyParser::default(), expected) } fn parse_relative_expected<'i, 'a>( input: &'i str, parse_relative: ParseRelative, expected: Option<&'a str>, ) -> Result, SelectorParseError<'i>> { parse_ns_relative_expected(input, &DummyParser::default(), parse_relative, expected) } fn parse_ns<'i>( input: &'i str, parser: &DummyParser, ) -> Result, SelectorParseError<'i>> { parse_ns_relative(input, parser, ParseRelative::No) } fn parse_ns_relative<'i>( input: &'i str, parser: &DummyParser, parse_relative: ParseRelative, ) -> Result, SelectorParseError<'i>> { parse_ns_relative_expected(input, parser, parse_relative, None) } fn parse_ns_expected<'i, 'a>( input: &'i str, parser: &DummyParser, expected: Option<&'a str>, ) -> Result, SelectorParseError<'i>> { parse_ns_relative_expected(input, parser, ParseRelative::No, expected) } fn parse_ns_relative_expected<'i, 'a>( input: &'i str, parser: &DummyParser, parse_relative: ParseRelative, expected: Option<&'a str>, ) -> Result, SelectorParseError<'i>> { let mut parser_input = ParserInput::new(input); let result = SelectorList::parse( parser, &mut CssParser::new(&mut parser_input), parse_relative, ); if let Ok(ref selectors) = result { // We can't assume that the serialized parsed selector will equal // the input; for example, if there is no default namespace, '*|foo' // should serialize to 'foo'. assert_eq!( selectors.to_css_string(), match expected { Some(x) => x, None => input, } ); } result } fn specificity(a: u32, b: u32, c: u32) -> u32 { a << 20 | b << 10 | c } #[test] fn test_empty() { let mut input = ParserInput::new(":empty"); let list = SelectorList::parse( &DummyParser::default(), &mut CssParser::new(&mut input), ParseRelative::No, ); assert!(list.is_ok()); } const MATHML: &str = "http://www.w3.org/1998/Math/MathML"; const SVG: &str = "http://www.w3.org/2000/svg"; #[test] fn test_parsing() { assert!(parse("").is_err()); assert!(parse(":lang(4)").is_err()); assert!(parse(":lang(en US)").is_err()); assert_eq!( parse("EeÉ"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::LocalName(LocalName { name: DummyAtom::from("EeÉ"), lower_name: DummyAtom::from("eeÉ"), })], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse("|e"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ExplicitNoNamespace, Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), ], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); // When the default namespace is not set, *| should be elided. // https://github.com/servo/servo/pull/17537 assert_eq!( parse_expected("*|e", Some("e")), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), })], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); // When the default namespace is set, *| should _not_ be elided (as foo // is no longer equivalent to *|foo--the former is only for foo in the // default namespace). // https://github.com/servo/servo/issues/16020 assert_eq!( parse_ns( "*|e", &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) ), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ExplicitAnyNamespace, Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), ], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse("*"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::ExplicitUniversalType], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse("|*"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ExplicitNoNamespace, Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_expected("*|*", Some("*")), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::ExplicitUniversalType], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns( "*|*", &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) ), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ExplicitAnyNamespace, Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse(".foo:lang(en-US)"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Class(DummyAtom::from("foo")), Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())), ], specificity(0, 2, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse("#bar"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::ID(DummyAtom::from("bar"))], specificity(1, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse("e.foo#bar"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), Component::Class(DummyAtom::from("foo")), Component::ID(DummyAtom::from("bar")), ], specificity(1, 1, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse("e.foo #bar"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), Component::Class(DummyAtom::from("foo")), Component::Combinator(Combinator::Descendant), Component::ID(DummyAtom::from("bar")), ], specificity(1, 1, 1), SelectorFlags::empty(), )])) ); // Default namespace does not apply to attribute selectors // https://github.com/mozilla/servo/pull/1652 let mut parser = DummyParser::default(); assert_eq!( parse_ns("[Foo]", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::AttributeInNoNamespaceExists { local_name: DummyAtom::from("Foo"), local_name_lower: DummyAtom::from("foo"), }], specificity(0, 1, 0), SelectorFlags::empty(), )])) ); assert!(parse_ns("svg|circle", &parser).is_err()); parser .ns_prefixes .insert(DummyAtom("svg".into()), DummyAtom(SVG.into())); assert_eq!( parse_ns("svg|circle", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Namespace(DummyAtom("svg".into()), SVG.into()), Component::LocalName(LocalName { name: DummyAtom::from("circle"), lower_name: DummyAtom::from("circle"), }), ], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns("svg|*", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Namespace(DummyAtom("svg".into()), SVG.into()), Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); // Default namespace does not apply to attribute selectors // https://github.com/mozilla/servo/pull/1652 // but it does apply to implicit type selectors // https://github.com/servo/rust-selectors/pull/82 parser.default_ns = Some(MATHML.into()); assert_eq!( parse_ns("[Foo]", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::AttributeInNoNamespaceExists { local_name: DummyAtom::from("Foo"), local_name_lower: DummyAtom::from("foo"), }, ], specificity(0, 1, 0), SelectorFlags::empty(), )])) ); // Default namespace does apply to type selectors assert_eq!( parse_ns("e", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), ], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns("*", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns("*|*", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ExplicitAnyNamespace, Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); // Default namespace applies to universal and type selectors inside :not and :matches, // but not otherwise. assert_eq!( parse_ns(":not(.cl)", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::Class(DummyAtom::from("cl"))], specificity(0, 1, 0), SelectorFlags::empty(), )])), ], specificity(0, 1, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns(":not(*)", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), )]),), ], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns(":not(e)", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::DefaultNamespace(MATHML.into()), Component::LocalName(LocalName { name: DummyAtom::from("e"), lower_name: DummyAtom::from("e"), }), ], specificity(0, 0, 1), SelectorFlags::empty(), )])), ], specificity(0, 0, 1), SelectorFlags::empty(), )])) ); assert_eq!( parse("[attr|=\"foo\"]"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::AttributeInNoNamespace { local_name: DummyAtom::from("attr"), operator: AttrSelectorOperator::DashMatch, value: DummyAttrValue::from("foo"), case_sensitivity: ParsedCaseSensitivity::CaseSensitive, }], specificity(0, 1, 0), SelectorFlags::empty(), )])) ); // https://github.com/mozilla/servo/issues/1723 assert_eq!( parse("::before"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Combinator(Combinator::PseudoElement), Component::PseudoElement(PseudoElement::Before), ], specificity(0, 0, 1), SelectorFlags::HAS_PSEUDO, )])) ); assert_eq!( parse("::before:hover"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Combinator(Combinator::PseudoElement), Component::PseudoElement(PseudoElement::Before), Component::NonTSPseudoClass(PseudoClass::Hover), ], specificity(0, 1, 1), SelectorFlags::HAS_PSEUDO, )])) ); assert_eq!( parse("::before:hover:hover"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Combinator(Combinator::PseudoElement), Component::PseudoElement(PseudoElement::Before), Component::NonTSPseudoClass(PseudoClass::Hover), Component::NonTSPseudoClass(PseudoClass::Hover), ], specificity(0, 2, 1), SelectorFlags::HAS_PSEUDO, )])) ); assert!(parse("::before:hover:lang(foo)").is_err()); assert!(parse("::before:hover .foo").is_err()); assert!(parse("::before .foo").is_err()); assert!(parse("::before ~ bar").is_err()); assert!(parse("::before:active").is_ok()); // https://github.com/servo/servo/issues/15335 assert!(parse(":: before").is_err()); assert_eq!( parse("div ::after"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::LocalName(LocalName { name: DummyAtom::from("div"), lower_name: DummyAtom::from("div"), }), Component::Combinator(Combinator::Descendant), Component::Combinator(Combinator::PseudoElement), Component::PseudoElement(PseudoElement::After), ], specificity(0, 0, 2), SelectorFlags::HAS_PSEUDO, )])) ); assert_eq!( parse("#d1 > .ok"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ID(DummyAtom::from("d1")), Component::Combinator(Combinator::Child), Component::Class(DummyAtom::from("ok")), ], specificity(1, 1, 0), SelectorFlags::empty(), )])) ); parser.default_ns = None; assert!(parse(":not(#provel.old)").is_ok()); assert!(parse(":not(#provel > old)").is_ok()); assert!(parse("table[rules]:not([rules=\"none\"]):not([rules=\"\"])").is_ok()); // https://github.com/servo/servo/issues/16017 assert_eq!( parse_ns(":not(*)", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::Negation(SelectorList::from_vec(vec![ Selector::from_vec( vec![Component::ExplicitUniversalType], specificity(0, 0, 0), SelectorFlags::empty(), ) ]))], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert_eq!( parse_ns(":not(|*)", &parser), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::Negation(SelectorList::from_vec(vec![ Selector::from_vec( vec![ Component::ExplicitNoNamespace, Component::ExplicitUniversalType, ], specificity(0, 0, 0), SelectorFlags::empty(), ) ]))], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); // *| should be elided if there is no default namespace. // https://github.com/servo/servo/pull/17537 assert_eq!( parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![Component::Negation(SelectorList::from_vec(vec![ Selector::from_vec( vec![Component::ExplicitUniversalType], specificity(0, 0, 0), SelectorFlags::empty(), ) ]))], specificity(0, 0, 0), SelectorFlags::empty(), )])) ); assert!(parse("::highlight(foo)").is_ok()); assert!(parse("::slotted()").is_err()); assert!(parse("::slotted(div)").is_ok()); assert!(parse("::slotted(div).foo").is_err()); assert!(parse("::slotted(div + bar)").is_err()); assert!(parse("::slotted(div) + foo").is_err()); assert!(parse("::part()").is_err()); assert!(parse("::part(42)").is_err()); assert!(parse("::part(foo bar)").is_ok()); assert!(parse("::part(foo):hover").is_ok()); assert!(parse("::part(foo) + bar").is_err()); assert!(parse("div ::slotted(div)").is_ok()); assert!(parse("div + slot::slotted(div)").is_ok()); assert!(parse("div + slot::slotted(div.foo)").is_ok()); assert!(parse("slot::slotted(div,foo)::first-line").is_err()); assert!(parse("::slotted(div)::before").is_ok()); assert!(parse("slot::slotted(div,foo)").is_err()); assert!(parse("foo:where()").is_ok()); assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); assert!(parse("foo:where(::before)").is_ok()); } #[test] fn parent_selector() { assert!(parse("foo &").is_ok()); assert_eq!( parse("#foo &.bar"), Ok(SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ID(DummyAtom::from("foo")), Component::Combinator(Combinator::Descendant), Component::ParentSelector, Component::Class(DummyAtom::from("bar")), ], specificity(1, 1, 0), SelectorFlags::HAS_PARENT )])) ); let parent = parse(".bar, div .baz").unwrap(); let child = parse("#foo &.bar").unwrap(); assert_eq!( child.replace_parent_selector(&parent), parse("#foo :is(.bar, div .baz).bar").unwrap() ); let has_child = parse("#foo:has(&.bar)").unwrap(); assert_eq!( has_child.replace_parent_selector(&parent), parse("#foo:has(:is(.bar, div .baz).bar)").unwrap() ); let child = parse_relative_expected("#foo", ParseRelative::ForNesting, Some("& #foo")).unwrap(); assert_eq!( child.replace_parent_selector(&parent), parse(":is(.bar, div .baz) #foo").unwrap() ); let child = parse_relative_expected("+ #foo", ParseRelative::ForNesting, Some("& + #foo")).unwrap(); assert_eq!(child, parse("& + #foo").unwrap()); } #[test] fn test_pseudo_iter() { let list = parse("q::before").unwrap(); let selector = &list.slice()[0]; assert!(!selector.is_universal()); let mut iter = selector.iter(); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)) ); assert_eq!(iter.next(), None); let combinator = iter.next_sequence(); assert_eq!(combinator, Some(Combinator::PseudoElement)); assert_eq!( iter.next(), Some(&Component::LocalName(LocalName { name: DummyAtom::from("q"), lower_name: DummyAtom::from("q"), })) ); assert_eq!(iter.next(), None); assert_eq!(iter.next_sequence(), None); } #[test] fn test_pseudo_before_marker() { let list = parse("::before::marker").unwrap(); let selector = &list.slice()[0]; let mut iter = selector.iter(); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::Marker)) ); assert_eq!(iter.next(), None); let combinator = iter.next_sequence(); assert_eq!(combinator, Some(Combinator::PseudoElement)); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)) ); assert_eq!(iter.next(), None); let combinator = iter.next_sequence(); assert_eq!(combinator, Some(Combinator::PseudoElement)); assert_eq!(iter.next(), None); assert_eq!(iter.next_sequence(), None); } #[test] fn test_pseudo_duplicate_before_after_or_marker() { assert!(parse("::before::before").is_err()); assert!(parse("::after::after").is_err()); assert!(parse("::marker::marker").is_err()); } #[test] fn test_pseudo_on_element_backed_pseudo() { let list = parse("::details-content::before").unwrap(); let selector = &list.slice()[0]; let mut iter = selector.iter(); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)) ); assert_eq!(iter.next(), None); let combinator = iter.next_sequence(); assert_eq!(combinator, Some(Combinator::PseudoElement)); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::DetailsContent)) ); assert_eq!(iter.next(), None); let combinator = iter.next_sequence(); assert_eq!(combinator, Some(Combinator::PseudoElement)); assert_eq!(iter.next(), None); assert_eq!(iter.next_sequence(), None); } #[test] fn test_universal() { let list = parse_ns( "*|*::before", &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")), ) .unwrap(); let selector = &list.slice()[0]; assert!(selector.is_universal()); } #[test] fn test_empty_pseudo_iter() { let list = parse("::before").unwrap(); let selector = &list.slice()[0]; assert!(selector.is_universal()); let mut iter = selector.iter(); assert_eq!( iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)) ); assert_eq!(iter.next(), None); assert_eq!(iter.next_sequence(), Some(Combinator::PseudoElement)); assert_eq!(iter.next(), None); assert_eq!(iter.next_sequence(), None); } #[test] fn test_parse_implicit_scope() { assert_eq!( parse_relative_expected(".foo", ParseRelative::ForScope, None).unwrap(), SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ImplicitScope, Component::Combinator(Combinator::Descendant), Component::Class(DummyAtom::from("foo")), ], specificity(0, 1, 0), SelectorFlags::HAS_SCOPE, )]) ); assert_eq!( parse_relative_expected(":scope .foo", ParseRelative::ForScope, None).unwrap(), SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Scope, Component::Combinator(Combinator::Descendant), Component::Class(DummyAtom::from("foo")), ], specificity(0, 2, 0), SelectorFlags::HAS_SCOPE )]) ); assert_eq!( parse_relative_expected("> .foo", ParseRelative::ForScope, Some("> .foo")).unwrap(), SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::ImplicitScope, Component::Combinator(Combinator::Child), Component::Class(DummyAtom::from("foo")), ], specificity(0, 1, 0), SelectorFlags::HAS_SCOPE )]) ); assert_eq!( parse_relative_expected(".foo :scope > .bar", ParseRelative::ForScope, None).unwrap(), SelectorList::from_vec(vec![Selector::from_vec( vec![ Component::Class(DummyAtom::from("foo")), Component::Combinator(Combinator::Descendant), Component::Scope, Component::Combinator(Combinator::Child), Component::Class(DummyAtom::from("bar")), ], specificity(0, 3, 0), SelectorFlags::HAS_SCOPE )]) ); } struct TestVisitor { seen: Vec, } impl SelectorVisitor for TestVisitor { type Impl = DummySelectorImpl; fn visit_simple_selector(&mut self, s: &Component) -> bool { let mut dest = String::new(); s.to_css(&mut dest).unwrap(); self.seen.push(dest); true } } #[test] fn visitor() { let mut test_visitor = TestVisitor { seen: vec![] }; parse(":not(:hover) ~ label").unwrap().slice()[0].visit(&mut test_visitor); assert!(test_visitor.seen.contains(&":hover".into())); let mut test_visitor = TestVisitor { seen: vec![] }; parse("::before:hover").unwrap().slice()[0].visit(&mut test_visitor); assert!(test_visitor.seen.contains(&":hover".into())); } } ================================================ FILE: selectors/relative_selector/cache.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use rustc_hash::FxHashMap; /// Relative selector cache. This is useful for following cases. /// First case is non-subject relative selector: Imagine `.anchor:has(<..>) ~ .foo`, with DOM /// `.anchor + .foo + .. + .foo`. Each match on `.foo` triggers `:has()` traversal that /// yields the same result. This is simple enough, since we just need to store /// the exact match on that anchor pass/fail. /// Second case is `querySelectorAll`: Imagine `querySelectorAll(':has(.a)')`, with DOM /// `div > .. > div > .a`. When the we perform the traversal at the top div, /// we basically end up evaluating `:has(.a)` for all anchors, which could be reused. /// Also consider the sibling version: `querySelectorAll(':has(~ .a)')` with DOM /// `div + .. + div + .a`. /// TODO(dshin): Second case is not yet handled. That is tracked in Bug 1845291. use std::hash::Hash; use crate::parser::{RelativeSelector, SelectorKey}; use crate::{tree::OpaqueElement, SelectorImpl}; /// Match data for a given element and a selector. #[derive(Clone, Copy)] pub enum RelativeSelectorCachedMatch { /// This selector matches this element. Matched, /// This selector does not match this element. NotMatched, } impl RelativeSelectorCachedMatch { /// Is the cached result a match? pub fn matched(&self) -> bool { matches!(*self, Self::Matched) } } #[derive(Clone, Copy, Hash, Eq, PartialEq)] struct Key { element: OpaqueElement, selector: SelectorKey, } impl Key { pub fn new( element: OpaqueElement, selector: &RelativeSelector, ) -> Self { Key { element, selector: SelectorKey::new(&selector.selector), } } } /// Cache to speed up matching of relative selectors. #[derive(Default)] pub struct RelativeSelectorCache { cache: FxHashMap, } impl RelativeSelectorCache { /// Add a relative selector match into the cache. pub fn add( &mut self, anchor: OpaqueElement, selector: &RelativeSelector, matched: RelativeSelectorCachedMatch, ) { self.cache.insert(Key::new(anchor, selector), matched); } /// Check if we have a cache entry for the element. pub fn lookup( &mut self, element: OpaqueElement, selector: &RelativeSelector, ) -> Option { self.cache.get(&Key::new(element, selector)).copied() } } ================================================ FILE: selectors/relative_selector/filter.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /// Bloom filter for relative selectors. use rustc_hash::FxHashMap; use crate::bloom::BloomFilter; use crate::context::QuirksMode; use crate::parser::{collect_selector_hashes, RelativeSelector, RelativeSelectorMatchHint}; use crate::tree::{Element, OpaqueElement}; use crate::SelectorImpl; enum Entry { /// Filter lookup happened once. Construction of the filter is expensive, /// so this is set when the element for subtree traversal is encountered. Lookup, /// Filter lookup happened more than once, and the filter for this element's /// subtree traversal is constructed. Could use special handlings for pseudo-classes /// such as `:hover` and `:focus`, see Bug 1845503. HasFilter(Box), } #[derive(Clone, Copy, Hash, Eq, PartialEq)] enum TraversalKind { Children, Descendants, } fn add_to_filter(element: &E, filter: &mut BloomFilter, kind: TraversalKind) -> bool { let mut child = element.first_element_child(); while let Some(e) = child { if !e.add_element_unique_hashes(filter) { return false; } if kind == TraversalKind::Descendants { if !add_to_filter(&e, filter, kind) { return false; } } child = e.next_sibling_element(); } true } #[derive(Clone, Copy, Hash, Eq, PartialEq)] struct Key(OpaqueElement, TraversalKind); /// Map of bloom filters for fast-rejecting relative selectors. #[derive(Default)] pub struct RelativeSelectorFilterMap { map: FxHashMap, } fn fast_reject( selector: &RelativeSelector, quirks_mode: QuirksMode, filter: &BloomFilter, ) -> bool { let mut hashes = [0u32; 4]; let mut len = 0; // For inner selectors, we only collect from the single rightmost compound. // This is because inner selectors can cause breakouts: e.g. `.anchor:has(:is(.a .b) .c)` // can match when `.a` is the ancestor of `.anchor`. Including `.a` would possibly fast // reject the subtree for not having `.a`, even if the selector would match. // Technically, if the selector's traversal is non-sibling subtree, we can traverse // inner selectors up to the point where a descendant/child combinator is encountered // (e.g. In `.anchor:has(:is(.a ~ .b) .c)`, `.a` is guaranteed to be the a descendant // of `.anchor`). While that can be separately handled, well, this is simpler. collect_selector_hashes( selector.selector.iter(), quirks_mode, &mut hashes, &mut len, |s| s.iter(), ); for i in 0..len { if !filter.might_contain_hash(hashes[i]) { // Definitely rejected. return true; } } false } impl RelativeSelectorFilterMap { fn get_filter(&mut self, element: &E, kind: TraversalKind) -> Option<&BloomFilter> { // Insert flag to indicate that we looked up the filter once, and // create the filter if and only if that flag is there. let key = Key(element.opaque(), kind); let entry = self .map .entry(key) .and_modify(|entry| { if !matches!(entry, Entry::Lookup) { return; } let mut filter = BloomFilter::new(); // Go through all children/descendants of this element and add their hashes. if add_to_filter(element, &mut filter, kind) { *entry = Entry::HasFilter(Box::new(filter)); } }) .or_insert(Entry::Lookup); match entry { Entry::Lookup => None, Entry::HasFilter(ref filter) => Some(filter.as_ref()), } } /// Potentially reject the given selector for this element. /// This may seem redundant in presence of the cache, but the cache keys into the /// selector-element pair specifically, while this filter keys to the element /// and the traversal kind, so it is useful for handling multiple selectors /// that effectively end up looking at the same(-ish, for siblings) subtree. pub fn fast_reject( &mut self, element: &E, selector: &RelativeSelector, quirks_mode: QuirksMode, ) -> bool { if matches!( selector.match_hint, RelativeSelectorMatchHint::InNextSibling ) { // Don't bother. return false; } let is_sibling = matches!( selector.match_hint, RelativeSelectorMatchHint::InSibling | RelativeSelectorMatchHint::InNextSiblingSubtree | RelativeSelectorMatchHint::InSiblingSubtree ); let is_subtree = matches!( selector.match_hint, RelativeSelectorMatchHint::InSubtree | RelativeSelectorMatchHint::InNextSiblingSubtree | RelativeSelectorMatchHint::InSiblingSubtree ); let kind = if is_subtree { TraversalKind::Descendants } else { TraversalKind::Children }; if is_sibling { // Contain the entirety of the parent's children/subtree in the filter, and use that. // This is less likely to reject, especially for sibling subtree matches; however, it's less // expensive memory-wise, compared to storing filters for each sibling. element.parent_element().map_or(false, |parent| { self.get_filter(&parent, kind) .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) }) } else { self.get_filter(element, kind) .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) } } } ================================================ FILE: selectors/relative_selector/mod.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ pub mod cache; pub mod filter; ================================================ FILE: selectors/sink.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Small helpers to abstract over different containers. #![deny(missing_docs)] use smallvec::{Array, SmallVec}; /// A trait to abstract over a `push` method that may be implemented for /// different kind of types. /// /// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a /// type which `push` method does only tweak a byte when we only need to check /// for the presence of something. pub trait Push { /// Push a value into self. fn push(&mut self, value: T); } impl Push for Vec { fn push(&mut self, value: T) { Vec::push(self, value); } } impl Push for SmallVec { fn push(&mut self, value: A::Item) { SmallVec::push(self, value); } } ================================================ FILE: selectors/tree.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Traits that nodes must implement. Breaks the otherwise-cyclic dependency //! between layout and style. use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; use crate::bloom::BloomFilter; use crate::matching::{ElementSelectorFlags, MatchingContext}; use crate::parser::SelectorImpl; use std::fmt::Debug; use std::ptr::NonNull; /// Opaque representation of an Element, for identity comparisons. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct OpaqueElement(NonNull<()>); unsafe impl Send for OpaqueElement {} // This should be safe given that we do not provide a way to recover // the original reference. unsafe impl Sync for OpaqueElement {} impl OpaqueElement { /// Creates a new OpaqueElement from an arbitrarily-typed pointer. pub fn new(ptr: &T) -> Self { unsafe { OpaqueElement(NonNull::new_unchecked( ptr as *const T as *const () as *mut (), )) } } /// Creates a new OpaqueElement from a type-erased non-null pointer pub fn from_non_null_ptr(ptr: NonNull<()>) -> Self { Self(ptr) } /// Returns a const ptr to the contained reference. Unsafe especially /// since Element can be recovered and potentially-mutated. pub unsafe fn as_const_ptr(&self) -> *const T { self.0.as_ptr() as *const T } } pub trait Element: Sized + Clone + Debug { type Impl: SelectorImpl; /// Converts self into an opaque representation. fn opaque(&self) -> OpaqueElement; fn parent_element(&self) -> Option; /// Whether the parent node of this element is a shadow root. fn parent_node_is_shadow_root(&self) -> bool; /// The host of the containing shadow root, if any. fn containing_shadow_host(&self) -> Option; /// The parent of a given pseudo-element, after matching a pseudo-element /// selector. /// /// This is guaranteed to be called in a pseudo-element. fn pseudo_element_originating_element(&self) -> Option { debug_assert!(self.is_pseudo_element()); self.parent_element() } /// Whether we're matching on a pseudo-element. fn is_pseudo_element(&self) -> bool; /// Skips non-element nodes fn prev_sibling_element(&self) -> Option; /// Skips non-element nodes fn next_sibling_element(&self) -> Option; /// Skips non-element nodes fn first_element_child(&self) -> Option; fn is_html_element_in_html_document(&self) -> bool; fn has_local_name(&self, local_name: &::BorrowedLocalName) -> bool; /// Empty string for no namespace fn has_namespace(&self, ns: &::BorrowedNamespaceUrl) -> bool; /// Whether this element and the `other` element have the same local name and namespace. fn is_same_type(&self, other: &Self) -> bool; fn attr_matches( &self, ns: &NamespaceConstraint<&::NamespaceUrl>, local_name: &::LocalName, operation: &AttrSelectorOperation<&::AttrValue>, ) -> bool; fn has_attr_in_no_namespace( &self, local_name: &::LocalName, ) -> bool { self.attr_matches( &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::()), local_name, &AttrSelectorOperation::Exists, ) } fn match_non_ts_pseudo_class( &self, pc: &::NonTSPseudoClass, context: &mut MatchingContext, ) -> bool; fn match_pseudo_element( &self, pe: &::PseudoElement, context: &mut MatchingContext, ) -> bool; /// Sets selector flags on the elemnt itself or the parent, depending on the /// flags, which indicate what kind of work may need to be performed when /// DOM state changes. fn apply_selector_flags(&self, flags: ElementSelectorFlags); /// Whether this element is a `link`. fn is_link(&self) -> bool; /// Returns whether the element is an HTML element. fn is_html_slot_element(&self) -> bool; /// Returns the assigned element this element is assigned to. /// /// Necessary for the `::slotted` pseudo-class. fn assigned_slot(&self) -> Option { None } fn has_id( &self, id: &::Identifier, case_sensitivity: CaseSensitivity, ) -> bool; fn has_class( &self, name: &::Identifier, case_sensitivity: CaseSensitivity, ) -> bool; fn has_custom_state(&self, name: &::Identifier) -> bool; /// Returns the mapping from the `exportparts` attribute in the reverse /// direction, that is, in an outer-tree -> inner-tree direction. fn imported_part( &self, name: &::Identifier, ) -> Option<::Identifier>; fn is_part(&self, name: &::Identifier) -> bool; /// Returns whether this element matches `:empty`. /// /// That is, whether it does not contain any child element or any non-zero-length text node. /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo fn is_empty(&self) -> bool; /// Returns whether this element matches `:root`, /// i.e. whether it is the root element of a document. /// /// Note: this can be false even if `.parent_element()` is `None` /// if the parent node is a `DocumentFragment`. fn is_root(&self) -> bool; /// Returns whether this element should ignore matching nth child /// selector. fn ignores_nth_child_selectors(&self) -> bool { false } /// Add hashes unique to this element to the given filter, returning true /// if any got added. fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool; } ================================================ FILE: selectors/visitor.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Visitor traits for selectors. #![deny(missing_docs)] use crate::attr::NamespaceConstraint; use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl}; use bitflags::bitflags; /// A trait to visit selector properties. /// /// All the `visit_foo` methods return a boolean indicating whether the /// traversal should continue or not. pub trait SelectorVisitor: Sized { /// The selector implementation this visitor wants to visit. type Impl: SelectorImpl; /// Visit an attribute selector that may match (there are other selectors /// that may never match, like those containing whitespace or the empty /// string). fn visit_attribute_selector( &mut self, _namespace: &NamespaceConstraint<&::NamespaceUrl>, _local_name: &::LocalName, _local_name_lower: &::LocalName, ) -> bool { true } /// Visit a simple selector. fn visit_simple_selector(&mut self, _: &Component) -> bool { true } /// Visit a nested relative selector list. The caller is responsible to call visit /// into the internal selectors if / as needed. /// /// The default implementation skips it altogether. fn visit_relative_selector_list(&mut self, _list: &[RelativeSelector]) -> bool { true } /// Visit a nested selector list. The caller is responsible to call visit /// into the internal selectors if / as needed. /// /// The default implementation does this. fn visit_selector_list( &mut self, _list_kind: SelectorListKind, list: &[Selector], ) -> bool { for nested in list { if !nested.visit(self) { return false; } } true } /// Visits a complex selector. /// /// Gets the combinator to the right of the selector, or `None` if the /// selector is the rightmost one. fn visit_complex_selector(&mut self, _combinator_to_right: Option) -> bool { true } } bitflags! { /// The kinds of components the visitor is visiting the selector list of, if any #[derive(Clone, Copy, Default)] pub struct SelectorListKind: u8 { /// The visitor is inside :not(..) const NEGATION = 1 << 0; /// The visitor is inside :is(..) const IS = 1 << 1; /// The visitor is inside :where(..) const WHERE = 1 << 2; /// The visitor is inside :nth-child(.. of ) or /// :nth-last-child(.. of ) const NTH_OF = 1 << 3; /// The visitor is inside :has(..) const HAS = 1 << 4; } } impl SelectorListKind { /// Construct a SelectorListKind for the corresponding component. pub fn from_component(component: &Component) -> Self { match component { Component::Negation(_) => SelectorListKind::NEGATION, Component::Is(_) => SelectorListKind::IS, Component::Where(_) => SelectorListKind::WHERE, Component::NthOf(_) => SelectorListKind::NTH_OF, _ => SelectorListKind::empty(), } } /// Whether the visitor is inside :not(..) pub fn in_negation(&self) -> bool { self.intersects(SelectorListKind::NEGATION) } /// Whether the visitor is inside :is(..) pub fn in_is(&self) -> bool { self.intersects(SelectorListKind::IS) } /// Whether the visitor is inside :where(..) pub fn in_where(&self) -> bool { self.intersects(SelectorListKind::WHERE) } /// Whether the visitor is inside :nth-child(.. of ) or /// :nth-last-child(.. of ) pub fn in_nth_of(&self) -> bool { self.intersects(SelectorListKind::NTH_OF) } /// Whether the visitor is inside :has(..) pub fn in_has(&self) -> bool { self.intersects(SelectorListKind::HAS) } /// Whether this nested selector is relevant for nth-of dependencies. pub fn relevant_to_nth_of_dependencies(&self) -> bool { // Order of nesting for `:has` and `:nth-child(.. of ..)` doesn't matter, because: // * `:has(:nth-child(.. of ..))`: The location of the anchoring element is // independent from where `:nth-child(.. of ..)` is applied. // * `:nth-child(.. of :has(..))`: Invalidations inside `:has` must first use the // `:has` machinary to find the anchor, then carry out the remaining invalidation. self.in_nth_of() && !self.in_has() } } ================================================ FILE: servo_arc/Cargo.toml ================================================ [package] name = "servo_arc" version = "0.4.3" authors = ["The Servo Project Developers"] license = "MIT OR Apache-2.0" repository = "https://github.com/servo/stylo" description = "A fork of std::sync::Arc with some extra functionality and without weak references" edition = "2021" readme = "../README.md" [lib] name = "servo_arc" path = "lib.rs" [features] default = ["track_alloc_size"] gecko_refcount_logging = [] servo = ["serde", "track_alloc_size"] track_alloc_size = [] [dependencies] serde = { version = "1.0", optional = true } stable_deref_trait = "1.0.0" ================================================ FILE: servo_arc/LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: servo_arc/LICENSE-MIT ================================================ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: servo_arc/lib.rs ================================================ // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Fork of Arc for Servo. This has the following advantages over std::sync::Arc: //! //! * We don't waste storage on the weak reference count. //! * We don't do extra RMU operations to handle the possibility of weak references. //! * We can experiment with arena allocation (todo). //! * We can add methods to support our custom use cases [1]. //! * We have support for dynamically-sized types (see from_header_and_iter). //! * We have support for thin arcs to unsized types (see ThinArc). //! * We have support for references to static data, which don't do any //! refcounting. //! //! [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1360883 // The semantics of `Arc` are already documented in the Rust docs, so we don't // duplicate those here. #![allow(missing_docs)] #[cfg(feature = "servo")] use serde::{Deserialize, Serialize}; use stable_deref_trait::{CloneStableDeref, StableDeref}; use std::alloc::{self, Layout}; use std::borrow; use std::cmp::Ordering; use std::fmt; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::mem::{self, align_of, size_of}; use std::ops::{Deref, DerefMut}; use std::os::raw::c_void; use std::process; use std::ptr; use std::sync::atomic; use std::sync::atomic::Ordering::{Acquire, Relaxed, Release}; use std::{isize, usize}; /// A soft limit on the amount of references that may be made to an `Arc`. /// /// Going above this limit will abort your program (although not /// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references. const MAX_REFCOUNT: usize = (isize::MAX) as usize; /// Special refcount value that means the data is not reference counted, /// and that the `Arc` is really acting as a read-only static reference. const STATIC_REFCOUNT: usize = usize::MAX; /// An atomically reference counted shared pointer /// /// See the documentation for [`Arc`] in the standard library. Unlike the /// standard library `Arc`, this `Arc` does not support weak reference counting. /// /// See the discussion in https://github.com/rust-lang/rust/pull/60594 for the /// usage of PhantomData. /// /// [`Arc`]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html /// /// cbindgen:derive-eq=false /// cbindgen:derive-neq=false #[repr(C)] pub struct Arc { p: ptr::NonNull>, phantom: PhantomData, } /// An `Arc` that is known to be uniquely owned /// /// When `Arc`s are constructed, they are known to be /// uniquely owned. In such a case it is safe to mutate /// the contents of the `Arc`. Normally, one would just handle /// this by mutating the data on the stack before allocating the /// `Arc`, however it's possible the data is large or unsized /// and you need to heap-allocate it earlier in such a way /// that it can be freely converted into a regular `Arc` once you're /// done. /// /// `UniqueArc` exists for this purpose, when constructed it performs /// the same allocations necessary for an `Arc`, however it allows mutable access. /// Once the mutation is finished, you can call `.shareable()` and get a regular `Arc` /// out of it. /// /// Ignore the doctest below there's no way to skip building with refcount /// logging during doc tests (see rust-lang/rust#45599). /// /// ```rust,ignore /// # use servo_arc::UniqueArc; /// let data = [1, 2, 3, 4, 5]; /// let mut x = UniqueArc::new(data); /// x[4] = 7; // mutate! /// let y = x.shareable(); // y is an Arc /// ``` pub struct UniqueArc(Arc); impl UniqueArc { #[inline] /// Construct a new UniqueArc pub fn new(data: T) -> Self { UniqueArc(Arc::new(data)) } /// Construct an uninitialized arc #[inline] pub fn new_uninit() -> UniqueArc> { unsafe { let layout = Layout::new::>>(); let ptr = alloc::alloc(layout); let mut p = ptr::NonNull::new(ptr) .unwrap_or_else(|| alloc::handle_alloc_error(layout)) .cast::>>(); ptr::write(&mut p.as_mut().count, atomic::AtomicUsize::new(1)); #[cfg(feature = "track_alloc_size")] ptr::write(&mut p.as_mut().alloc_size, layout.size()); #[cfg(feature = "gecko_refcount_logging")] { NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) } UniqueArc(Arc { p, phantom: PhantomData, }) } } #[inline] /// Convert to a shareable Arc once we're done mutating it pub fn shareable(self) -> Arc { self.0 } } impl UniqueArc> { /// Convert to an initialized Arc. #[inline] pub unsafe fn assume_init(this: Self) -> UniqueArc { UniqueArc(Arc { p: mem::ManuallyDrop::new(this).0.p.cast(), phantom: PhantomData, }) } } impl Deref for UniqueArc { type Target = T; fn deref(&self) -> &T { &*self.0 } } impl DerefMut for UniqueArc { fn deref_mut(&mut self) -> &mut T { // We know this to be uniquely owned unsafe { &mut (*self.0.ptr()).data } } } unsafe impl Send for Arc {} unsafe impl Sync for Arc {} /// The object allocated by an Arc /// /// See https://github.com/mozilla/cbindgen/issues/937 for the derive-{eq,neq}=false. But we don't /// use those anyways so we can just disable them. /// cbindgen:derive-eq=false /// cbindgen:derive-neq=false #[repr(C)] struct ArcInner { count: atomic::AtomicUsize, // NOTE(emilio): This needs to be here so that HeaderSlice<> is deallocated properly if the // allocator relies on getting the right Layout. We don't need to track the right alignment, // since we know that statically. // // This member could be completely avoided once min_specialization feature is stable (by // implementing a trait for HeaderSlice that gives you the right layout). For now, servo-only // since Gecko doesn't need it (its allocator doesn't need the size for the alignments we care // about). See https://github.com/rust-lang/rust/issues/31844. #[cfg(feature = "track_alloc_size")] alloc_size: usize, data: T, } unsafe impl Send for ArcInner {} unsafe impl Sync for ArcInner {} /// Computes the offset of the data field within ArcInner. fn data_offset() -> usize { let size = size_of::>(); let align = align_of::(); // https://github.com/rust-lang/rust/blob/1.36.0/src/libcore/alloc.rs#L187-L207 size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1) } impl Arc { /// Construct an `Arc` #[inline] pub fn new(data: T) -> Self { let layout = Layout::new::>(); let p = unsafe { let ptr = ptr::NonNull::new(alloc::alloc(layout)) .unwrap_or_else(|| alloc::handle_alloc_error(layout)) .cast::>(); ptr::write( ptr.as_ptr(), ArcInner { count: atomic::AtomicUsize::new(1), #[cfg(feature = "track_alloc_size")] alloc_size: layout.size(), data, }, ); ptr }; #[cfg(feature = "gecko_refcount_logging")] unsafe { // FIXME(emilio): Would be so amazing to have // std::intrinsics::type_name() around, so that we could also report // a real size. NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); } Arc { p, phantom: PhantomData, } } /// Construct an intentionally-leaked arc. #[inline] pub fn new_leaked(data: T) -> Self { let arc = Self::new(data); arc.mark_as_intentionally_leaked(); arc } /// Convert the Arc to a raw pointer, suitable for use across FFI /// /// Note: This returns a pointer to the data T, which is offset in the allocation. #[inline] pub fn into_raw(this: Self) -> *const T { let ptr = unsafe { &((*this.ptr()).data) as *const _ }; mem::forget(this); ptr } /// Reconstruct the Arc from a raw pointer obtained from into_raw() /// /// Note: This raw pointer will be offset in the allocation and must be preceded /// by the atomic count. #[inline] pub unsafe fn from_raw(ptr: *const T) -> Self { // To find the corresponding pointer to the `ArcInner` we need // to subtract the offset of the `data` field from the pointer. let ptr = (ptr as *const u8).sub(data_offset::()); Arc { p: ptr::NonNull::new_unchecked(ptr as *mut ArcInner), phantom: PhantomData, } } /// Like from_raw, but returns an addrefed arc instead. #[inline] pub unsafe fn from_raw_addrefed(ptr: *const T) -> Self { let arc = Self::from_raw(ptr); mem::forget(arc.clone()); arc } /// Create a new static Arc (one that won't reference count the object) /// and place it in the allocation provided by the specified `alloc` /// function. /// /// `alloc` must return a pointer into a static allocation suitable for /// storing data with the `Layout` passed into it. The pointer returned by /// `alloc` will not be freed. #[inline] pub unsafe fn new_static(alloc: F, data: T) -> Arc where F: FnOnce(Layout) -> *mut u8, { let layout = Layout::new::>(); let ptr = alloc(layout) as *mut ArcInner; let x = ArcInner { count: atomic::AtomicUsize::new(STATIC_REFCOUNT), #[cfg(feature = "track_alloc_size")] alloc_size: layout.size(), data, }; ptr::write(ptr, x); Arc { p: ptr::NonNull::new_unchecked(ptr), phantom: PhantomData, } } /// Produce a pointer to the data that can be converted back /// to an Arc. This is basically an `&Arc`, without the extra indirection. /// It has the benefits of an `&T` but also knows about the underlying refcount /// and can be converted into more `Arc`s if necessary. #[inline] pub fn borrow_arc<'a>(&'a self) -> ArcBorrow<'a, T> { ArcBorrow(&**self) } /// Returns the address on the heap of the Arc itself -- not the T within it -- for memory /// reporting. /// /// If this is a static reference, this returns null. pub fn heap_ptr(&self) -> *const c_void { if self.inner().count.load(Relaxed) == STATIC_REFCOUNT { ptr::null() } else { self.p.as_ptr() as *const ArcInner as *const c_void } } } impl Arc { #[inline] fn inner(&self) -> &ArcInner { // This unsafety is ok because while this arc is alive we're guaranteed // that the inner pointer is valid. Furthermore, we know that the // `ArcInner` structure itself is `Sync` because the inner data is // `Sync` as well, so we're ok loaning out an immutable pointer to these // contents. unsafe { &*self.ptr() } } #[inline(always)] fn record_drop(&self) { #[cfg(feature = "gecko_refcount_logging")] unsafe { NS_LogDtor(self.ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); } } /// Marks this `Arc` as intentionally leaked for the purposes of refcount /// logging. /// /// It's a logic error to call this more than once, but it's not unsafe, as /// it'd just report negative leaks. #[inline(always)] pub fn mark_as_intentionally_leaked(&self) { self.record_drop(); } // Non-inlined part of `drop`. Just invokes the destructor and calls the // refcount logging machinery if enabled. #[inline(never)] unsafe fn drop_slow(&mut self) { self.record_drop(); let inner = self.ptr(); let layout = Layout::for_value(&*inner); #[cfg(feature = "track_alloc_size")] let layout = Layout::from_size_align_unchecked((*inner).alloc_size, layout.align()); std::ptr::drop_in_place(inner); alloc::dealloc(inner as *mut _, layout); } /// Test pointer equality between the two Arcs, i.e. they must be the _same_ /// allocation #[inline] pub fn ptr_eq(this: &Self, other: &Self) -> bool { this.raw_ptr() == other.raw_ptr() } fn ptr(&self) -> *mut ArcInner { self.p.as_ptr() } /// Returns a raw ptr to the underlying allocation. pub fn raw_ptr(&self) -> ptr::NonNull<()> { self.p.cast() } } #[cfg(feature = "gecko_refcount_logging")] extern "C" { fn NS_LogCtor( aPtr: *mut std::os::raw::c_void, aTypeName: *const std::os::raw::c_char, aSize: u32, ); fn NS_LogDtor( aPtr: *mut std::os::raw::c_void, aTypeName: *const std::os::raw::c_char, aSize: u32, ); } impl Clone for Arc { #[inline] fn clone(&self) -> Self { // NOTE(emilio): If you change anything here, make sure that the // implementation in layout/style/ServoStyleConstsInlines.h matches! // // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since // `count` never changes between STATIC_REFCOUNT and other values. if self.inner().count.load(Relaxed) != STATIC_REFCOUNT { // Using a relaxed ordering is alright here, as knowledge of the // original reference prevents other threads from erroneously deleting // the object. // // As explained in the [Boost documentation][1], Increasing the // reference counter can always be done with memory_order_relaxed: New // references to an object can only be formed from an existing // reference, and passing an existing reference from one thread to // another must already provide any required synchronization. // // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) let old_size = self.inner().count.fetch_add(1, Relaxed); // However we need to guard against massive refcounts in case someone // is `mem::forget`ing Arcs. If we don't do this the count can overflow // and users will use-after free. We racily saturate to `isize::MAX` on // the assumption that there aren't ~2 billion threads incrementing // the reference count at once. This branch will never be taken in // any realistic program. // // We abort because such a program is incredibly degenerate, and we // don't care to support it. if old_size > MAX_REFCOUNT { process::abort(); } } unsafe { Arc { p: ptr::NonNull::new_unchecked(self.ptr()), phantom: PhantomData, } } } } impl Deref for Arc { type Target = T; #[inline] fn deref(&self) -> &T { &self.inner().data } } impl Arc { /// Makes a mutable reference to the `Arc`, cloning if necessary /// /// This is functionally equivalent to [`Arc::make_mut`][mm] from the standard library. /// /// If this `Arc` is uniquely owned, `make_mut()` will provide a mutable /// reference to the contents. If not, `make_mut()` will create a _new_ `Arc` /// with a copy of the contents, update `this` to point to it, and provide /// a mutable reference to its contents. /// /// This is useful for implementing copy-on-write schemes where you wish to /// avoid copying things if your `Arc` is not shared. /// /// [mm]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.make_mut #[inline] pub fn make_mut(this: &mut Self) -> &mut T { if !this.is_unique() { // Another pointer exists; clone *this = Arc::new((**this).clone()); } unsafe { // This unsafety is ok because we're guaranteed that the pointer // returned is the *only* pointer that will ever be returned to T. Our // reference count is guaranteed to be 1 at this point, and we required // the Arc itself to be `mut`, so we're returning the only possible // reference to the inner data. &mut (*this.ptr()).data } } } impl Arc { /// Provides mutable access to the contents _if_ the `Arc` is uniquely owned. #[inline] pub fn get_mut(this: &mut Self) -> Option<&mut T> { if this.is_unique() { unsafe { // See make_mut() for documentation of the threadsafety here. Some(&mut (*this.ptr()).data) } } else { None } } /// Whether or not the `Arc` is a static reference. #[inline] pub fn is_static(&self) -> bool { // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since // `count` never changes between STATIC_REFCOUNT and other values. self.inner().count.load(Relaxed) == STATIC_REFCOUNT } /// Whether or not the `Arc` is uniquely owned (is the refcount 1?) and not /// a static reference. #[inline] pub fn is_unique(&self) -> bool { // See the extensive discussion in [1] for why this needs to be Acquire. // // [1] https://github.com/servo/servo/issues/21186 self.inner().count.load(Acquire) == 1 } } impl Drop for Arc { #[inline] fn drop(&mut self) { // NOTE(emilio): If you change anything here, make sure that the // implementation in layout/style/ServoStyleConstsInlines.h matches! if self.is_static() { return; } // Because `fetch_sub` is already atomic, we do not need to synchronize // with other threads unless we are going to delete the object. if self.inner().count.fetch_sub(1, Release) != 1 { return; } // FIXME(bholley): Use the updated comment when [2] is merged. // // This load is needed to prevent reordering of use of the data and // deletion of the data. Because it is marked `Release`, the decreasing // of the reference count synchronizes with this `Acquire` load. This // means that use of the data happens before decreasing the reference // count, which happens before this load, which happens before the // deletion of the data. // // As explained in the [Boost documentation][1], // // > It is important to enforce any possible access to the object in one // > thread (through an existing reference) to *happen before* deleting // > the object in a different thread. This is achieved by a "release" // > operation after dropping a reference (any access to the object // > through this reference must obviously happened before), and an // > "acquire" operation before deleting the object. // // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) // [2]: https://github.com/rust-lang/rust/pull/41714 self.inner().count.load(Acquire); unsafe { self.drop_slow(); } } } impl PartialEq for Arc { fn eq(&self, other: &Arc) -> bool { Self::ptr_eq(self, other) || *(*self) == *(*other) } fn ne(&self, other: &Arc) -> bool { !Self::ptr_eq(self, other) && *(*self) != *(*other) } } impl PartialOrd for Arc { fn partial_cmp(&self, other: &Arc) -> Option { (**self).partial_cmp(&**other) } fn lt(&self, other: &Arc) -> bool { *(*self) < *(*other) } fn le(&self, other: &Arc) -> bool { *(*self) <= *(*other) } fn gt(&self, other: &Arc) -> bool { *(*self) > *(*other) } fn ge(&self, other: &Arc) -> bool { *(*self) >= *(*other) } } impl Ord for Arc { fn cmp(&self, other: &Arc) -> Ordering { (**self).cmp(&**other) } } impl Eq for Arc {} impl fmt::Display for Arc { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&**self, f) } } impl fmt::Debug for Arc { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(&**self, f) } } impl fmt::Pointer for Arc { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Pointer::fmt(&self.ptr(), f) } } impl Default for Arc { fn default() -> Arc { Arc::new(Default::default()) } } impl Hash for Arc { fn hash(&self, state: &mut H) { (**self).hash(state) } } impl From for Arc { #[inline] fn from(t: T) -> Self { Arc::new(t) } } impl borrow::Borrow for Arc { #[inline] fn borrow(&self) -> &T { &**self } } impl AsRef for Arc { #[inline] fn as_ref(&self) -> &T { &**self } } unsafe impl StableDeref for Arc {} unsafe impl CloneStableDeref for Arc {} #[cfg(feature = "servo")] impl<'de, T: Deserialize<'de>> Deserialize<'de> for Arc { fn deserialize(deserializer: D) -> Result, D::Error> where D: ::serde::de::Deserializer<'de>, { T::deserialize(deserializer).map(Arc::new) } } #[cfg(feature = "servo")] impl Serialize for Arc { fn serialize(&self, serializer: S) -> Result where S: ::serde::ser::Serializer, { (**self).serialize(serializer) } } /// Structure to allow Arc-managing some fixed-sized data and a variably-sized /// slice in a single allocation. /// /// cbindgen:derive-eq=false /// cbindgen:derive-neq=false #[derive(Eq)] #[repr(C)] pub struct HeaderSlice { /// The fixed-sized data. pub header: H, /// The length of the slice at our end. len: usize, /// The dynamically-sized data. data: [T; 0], } impl PartialEq for HeaderSlice { fn eq(&self, other: &Self) -> bool { self.header == other.header && self.slice() == other.slice() } } impl Drop for HeaderSlice { fn drop(&mut self) { unsafe { let mut ptr = self.data_mut(); for _ in 0..self.len { std::ptr::drop_in_place(ptr); ptr = ptr.offset(1); } } } } impl fmt::Debug for HeaderSlice { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("HeaderSlice") .field("header", &self.header) .field("slice", &self.slice()) .finish() } } impl HeaderSlice { /// Returns the dynamically sized slice in this HeaderSlice. #[inline(always)] pub fn slice(&self) -> &[T] { unsafe { std::slice::from_raw_parts(self.data(), self.len) } } #[inline(always)] fn data(&self) -> *const T { std::ptr::addr_of!(self.data) as *const _ } #[inline(always)] fn data_mut(&mut self) -> *mut T { std::ptr::addr_of_mut!(self.data) as *mut _ } /// Returns the dynamically sized slice in this HeaderSlice. #[inline(always)] pub fn slice_mut(&mut self) -> &mut [T] { unsafe { std::slice::from_raw_parts_mut(self.data_mut(), self.len) } } /// Returns the len of the slice. #[inline(always)] pub fn len(&self) -> usize { self.len } } impl Arc> { /// Creates an Arc for a HeaderSlice using the given header struct and /// iterator to generate the slice. /// /// `is_static` indicates whether to create a static Arc. /// /// `alloc` is used to get a pointer to the memory into which the /// dynamically sized ArcInner> value will be /// written. If `is_static` is true, then `alloc` must return a /// pointer into some static memory allocation. If it is false, /// then `alloc` must return an allocation that can be dellocated /// by calling Box::from_raw::>> on it. #[inline] pub fn from_header_and_iter_alloc( alloc: F, header: H, mut items: I, num_items: usize, is_static: bool, ) -> Self where F: FnOnce(Layout) -> *mut u8, I: Iterator, { assert_ne!(size_of::(), 0, "Need to think about ZST"); let layout = Layout::new::>>(); debug_assert!(layout.align() >= align_of::()); debug_assert!(layout.align() >= align_of::()); let array_layout = Layout::array::(num_items).expect("Overflow"); let (layout, _offset) = layout.extend(array_layout).expect("Overflow"); let p = unsafe { // Allocate the buffer. let buffer = alloc(layout); let mut p = ptr::NonNull::new(buffer) .unwrap_or_else(|| alloc::handle_alloc_error(layout)) .cast::>>(); // Write the data. // // Note that any panics here (i.e. from the iterator) are safe, since // we'll just leak the uninitialized memory. let count = if is_static { atomic::AtomicUsize::new(STATIC_REFCOUNT) } else { atomic::AtomicUsize::new(1) }; ptr::write(&mut p.as_mut().count, count); #[cfg(feature = "track_alloc_size")] ptr::write(&mut p.as_mut().alloc_size, layout.size()); ptr::write(&mut p.as_mut().data.header, header); ptr::write(&mut p.as_mut().data.len, num_items); if num_items != 0 { let mut current = std::ptr::addr_of_mut!(p.as_mut().data.data) as *mut T; for _ in 0..num_items { ptr::write( current, items .next() .expect("ExactSizeIterator over-reported length"), ); current = current.offset(1); } // We should have consumed the buffer exactly, maybe accounting // for some padding from the alignment. debug_assert!( (buffer.add(layout.size()) as usize - current as *mut u8 as usize) < layout.align() ); } assert!( items.next().is_none(), "ExactSizeIterator under-reported length" ); p }; #[cfg(feature = "gecko_refcount_logging")] unsafe { if !is_static { // FIXME(emilio): Would be so amazing to have // std::intrinsics::type_name() around. NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) } } // Return the fat Arc. assert_eq!( size_of::(), size_of::(), "The Arc should be thin" ); Arc { p, phantom: PhantomData, } } /// Creates an Arc for a HeaderSlice using the given header struct and iterator to generate the /// slice. Panics if num_items doesn't match the number of items. #[inline] pub fn from_header_and_iter_with_size(header: H, items: I, num_items: usize) -> Self where I: Iterator, { Arc::from_header_and_iter_alloc( |layout| unsafe { alloc::alloc(layout) }, header, items, num_items, /* is_static = */ false, ) } /// Creates an Arc for a HeaderSlice using the given header struct and /// iterator to generate the slice. The resulting Arc will be fat. #[inline] pub fn from_header_and_iter(header: H, items: I) -> Self where I: Iterator + ExactSizeIterator, { let len = items.len(); Self::from_header_and_iter_with_size(header, items, len) } } /// This is functionally equivalent to Arc<(H, [T])> /// /// When you create an `Arc` containing a dynamically sized type like a slice, the `Arc` is /// represented on the stack as a "fat pointer", where the length of the slice is stored alongside /// the `Arc`'s pointer. In some situations you may wish to have a thin pointer instead, perhaps /// for FFI compatibility or space efficiency. `ThinArc` solves this by storing the length in the /// allocation itself, via `HeaderSlice`. pub type ThinArc = Arc>; /// See `ArcUnion`. This is a version that works for `ThinArc`s. pub type ThinArcUnion = ArcUnion, HeaderSlice>; impl UniqueArc> { #[inline] pub fn from_header_and_iter(header: H, items: I) -> Self where I: Iterator + ExactSizeIterator, { Self(Arc::from_header_and_iter(header, items)) } #[inline] pub fn from_header_and_iter_with_size(header: H, items: I, num_items: usize) -> Self where I: Iterator, { Self(Arc::from_header_and_iter_with_size( header, items, num_items, )) } /// Returns a mutable reference to the header. pub fn header_mut(&mut self) -> &mut H { // We know this to be uniquely owned unsafe { &mut (*self.0.ptr()).data.header } } /// Returns a mutable reference to the slice. pub fn data_mut(&mut self) -> &mut [T] { // We know this to be uniquely owned unsafe { (*self.0.ptr()).data.slice_mut() } } } /// A "borrowed `Arc`". This is a pointer to /// a T that is known to have been allocated within an /// `Arc`. /// /// This is equivalent in guarantees to `&Arc`, however it is /// a bit more flexible. To obtain an `&Arc` you must have /// an `Arc` instance somewhere pinned down until we're done with it. /// It's also a direct pointer to `T`, so using this involves less pointer-chasing /// /// However, C++ code may hand us refcounted things as pointers to T directly, /// so we have to conjure up a temporary `Arc` on the stack each time. /// /// `ArcBorrow` lets us deal with borrows of known-refcounted objects /// without needing to worry about where the `Arc` is. #[derive(Debug, Eq, PartialEq)] pub struct ArcBorrow<'a, T: 'a>(&'a T); impl<'a, T> Copy for ArcBorrow<'a, T> {} impl<'a, T> Clone for ArcBorrow<'a, T> { #[inline] fn clone(&self) -> Self { *self } } impl<'a, T> ArcBorrow<'a, T> { /// Clone this as an `Arc`. This bumps the refcount. #[inline] pub fn clone_arc(&self) -> Arc { let arc = unsafe { Arc::from_raw(self.0) }; // addref it! mem::forget(arc.clone()); arc } /// For constructing from a reference known to be Arc-backed, /// e.g. if we obtain such a reference over FFI #[inline] pub unsafe fn from_ref(r: &'a T) -> Self { ArcBorrow(r) } /// Compare two `ArcBorrow`s via pointer equality. Will only return /// true if they come from the same allocation pub fn ptr_eq(this: &Self, other: &Self) -> bool { this.0 as *const T == other.0 as *const T } /// Temporarily converts |self| into a bonafide Arc and exposes it to the /// provided callback. The refcount is not modified. #[inline] pub fn with_arc(&self, f: F) -> U where F: FnOnce(&Arc) -> U, T: 'static, { // Synthesize transient Arc, which never touches the refcount. let transient = unsafe { mem::ManuallyDrop::new(Arc::from_raw(self.0)) }; // Expose the transient Arc to the callback, which may clone it if it wants. let result = f(&transient); // Forward the result. result } /// Similar to deref, but uses the lifetime |a| rather than the lifetime of /// self, which is incompatible with the signature of the Deref trait. #[inline] pub fn get(&self) -> &'a T { self.0 } } impl<'a, T> Deref for ArcBorrow<'a, T> { type Target = T; #[inline] fn deref(&self) -> &T { self.0 } } /// A tagged union that can represent `Arc` or `Arc` while only consuming a /// single word. The type is also `NonNull`, and thus can be stored in an Option /// without increasing size. /// /// This is functionally equivalent to /// `enum ArcUnion { First(Arc), Second(Arc)` but only takes up /// up a single word of stack space. /// /// This could probably be extended to support four types if necessary. pub struct ArcUnion { p: ptr::NonNull<()>, phantom_a: PhantomData, phantom_b: PhantomData, } unsafe impl Send for ArcUnion {} unsafe impl Sync for ArcUnion {} impl PartialEq for ArcUnion { fn eq(&self, other: &Self) -> bool { use crate::ArcUnionBorrow::*; match (self.borrow(), other.borrow()) { (First(x), First(y)) => x == y, (Second(x), Second(y)) => x == y, (_, _) => false, } } } impl Eq for ArcUnion {} /// This represents a borrow of an `ArcUnion`. #[derive(Debug)] pub enum ArcUnionBorrow<'a, A: 'a, B: 'a> { First(ArcBorrow<'a, A>), Second(ArcBorrow<'a, B>), } impl ArcUnion { unsafe fn new(ptr: *mut ()) -> Self { ArcUnion { p: ptr::NonNull::new_unchecked(ptr), phantom_a: PhantomData, phantom_b: PhantomData, } } /// Returns true if the two values are pointer-equal. #[inline] pub fn ptr_eq(this: &Self, other: &Self) -> bool { this.p == other.p } #[inline] pub fn ptr(&self) -> ptr::NonNull<()> { self.p } /// Returns an enum representing a borrow of either A or B. #[inline] pub fn borrow(&self) -> ArcUnionBorrow<'_, A, B> { if self.is_first() { let ptr = self.p.as_ptr() as *const ArcInner; let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; ArcUnionBorrow::First(borrow) } else { let ptr = ((self.p.as_ptr() as usize) & !0x1) as *const ArcInner; let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; ArcUnionBorrow::Second(borrow) } } /// Creates an `ArcUnion` from an instance of the first type. pub fn from_first(other: Arc) -> Self { let union = unsafe { Self::new(other.ptr() as *mut _) }; mem::forget(other); union } /// Creates an `ArcUnion` from an instance of the second type. pub fn from_second(other: Arc) -> Self { let union = unsafe { Self::new(((other.ptr() as usize) | 0x1) as *mut _) }; mem::forget(other); union } /// Returns true if this `ArcUnion` contains the first type. pub fn is_first(&self) -> bool { self.p.as_ptr() as usize & 0x1 == 0 } /// Returns true if this `ArcUnion` contains the second type. pub fn is_second(&self) -> bool { !self.is_first() } /// Returns a borrow of the first type if applicable, otherwise `None`. pub fn as_first(&self) -> Option> { match self.borrow() { ArcUnionBorrow::First(x) => Some(x), ArcUnionBorrow::Second(_) => None, } } /// Returns a borrow of the second type if applicable, otherwise None. pub fn as_second(&self) -> Option> { match self.borrow() { ArcUnionBorrow::First(_) => None, ArcUnionBorrow::Second(x) => Some(x), } } } impl Clone for ArcUnion { fn clone(&self) -> Self { match self.borrow() { ArcUnionBorrow::First(x) => ArcUnion::from_first(x.clone_arc()), ArcUnionBorrow::Second(x) => ArcUnion::from_second(x.clone_arc()), } } } impl Drop for ArcUnion { fn drop(&mut self) { match self.borrow() { ArcUnionBorrow::First(x) => unsafe { let _ = Arc::from_raw(&*x); }, ArcUnionBorrow::Second(x) => unsafe { let _ = Arc::from_raw(&*x); }, } } } impl fmt::Debug for ArcUnion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(&self.borrow(), f) } } #[cfg(test)] mod tests { use super::{Arc, ThinArc}; use std::clone::Clone; use std::ops::Drop; use std::sync::atomic; use std::sync::atomic::Ordering::{Acquire, SeqCst}; #[derive(PartialEq)] struct Canary(*mut atomic::AtomicUsize); impl Drop for Canary { fn drop(&mut self) { unsafe { (*self.0).fetch_add(1, SeqCst); } } } #[test] fn empty_thin() { let x = Arc::from_header_and_iter(100u32, std::iter::empty::()); assert_eq!(x.header, 100); assert!(x.slice().is_empty()); } #[test] fn thin_assert_padding() { #[derive(Clone, Default)] #[repr(C)] struct Padded { i: u16, } // The header will have more alignment than `Padded` let items = vec![Padded { i: 0xdead }, Padded { i: 0xbeef }]; let a = ThinArc::from_header_and_iter(0i32, items.into_iter()); assert_eq!(a.len(), 2); assert_eq!(a.slice()[0].i, 0xdead); assert_eq!(a.slice()[1].i, 0xbeef); } #[test] fn slices_and_thin() { let mut canary = atomic::AtomicUsize::new(0); let c = Canary(&mut canary as *mut atomic::AtomicUsize); let v = vec![5, 6]; { let x = Arc::from_header_and_iter(c, v.into_iter()); let _ = x.clone(); let _ = x == x; } assert_eq!(canary.load(Acquire), 1); } } ================================================ FILE: shell.nix ================================================ with import (builtins.fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/46ae0210ce163b3cba6c7da08840c1d63de9c701.tar.gz"; }) {}; stdenv.mkDerivation rec { name = "style-sync-shell"; } ================================================ FILE: start-rebase.sh ================================================ #!/bin/sh # Usage: start-rebase.sh [extra git-rebase(1) arguments ...] # Equivalent to git rebase --onto . set -eu new_base=$1; shift first_commit=$(git log --pretty=\%H --grep='Servo initial downstream commit') old_base=$first_commit~ git rebase --onto "$new_base" "$old_base" "$@" ================================================ FILE: style/Cargo.toml ================================================ [package] name = "stylo" version.workspace = true authors = ["The Servo Project Developers"] license = "MPL-2.0" repository = "https://github.com/servo/stylo" edition = "2021" description = "The Stylo CSS engine" readme = "../README.md" build = "build.rs" # https://github.com/rust-lang/cargo/issues/3544 links = "servo_style_crate" [lib] name = "style" path = "lib.rs" doctest = false [features] default = ["servo"] gecko = [ "bindgen", "malloc_size_of/gecko", "mozbuild", "nsstring", "regex", "serde", "style_traits/gecko", "toml", "selectors/to_shmem", "to_shmem/gecko", ] servo = [ "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "web_atoms", "mime", "serde", "servo_arc/servo", "stylo_atoms", "string_cache", "style_traits/servo", "url", "selectors/to_shmem", "to_shmem/servo", ] gecko_debug = [] gecko_refcount_logging = [] nsstring = [] [dependencies] app_units = "0.7.8" arrayvec = "0.7" atomic_refcell = "0.1" bitflags = "2" byteorder = "1.0" cssparser = "0.37" derive_more = { version = "2", features = ["add", "add_assign", "deref", "deref_mut", "from"] } dom = { workspace = true } new_debug_unreachable = "1.0" encoding_rs = {version = "0.8", optional = true} euclid = "0.22" rustc-hash = "2.1.1" icu_segmenter = { version = ">= 1.5, <= 2.*", default-features = false, features = ["auto", "compiled_data"] } indexmap = {version = "2", features = ["std"]} itertools = "0.14" itoa = "1.0" log = "0.4" malloc_size_of = { workspace = true } malloc_size_of_derive = "0.1" web_atoms = { version = "0.2.0", optional = true } mime = { version = "0.3.13", optional = true } num_cpus = {version = "1.1.0"} num-integer = "0.1" num-traits = "0.2" num-derive = "0.4" parking_lot = "0.12" precomputed-hash = "0.1.1" rayon = "1" rayon-core = "1" selectors = { workspace = true } serde = {version = "1.0", optional = true, features = ["derive"]} servo_arc = { workspace = true} stylo_atoms = { workspace = true, optional = true} smallbitvec = "2.3.0" smallvec = "1.0" static_assertions = "1.1" static_prefs = { workspace = true} string_cache = { version = "0.9", optional = true } strum = "0.28" strum_macros = "0.28" style_derive = { workspace = true } style_traits = { workspace = true } to_shmem = { workspace = true} to_shmem_derive = { workspace = true } thin-vec = "0.2.1" uluru = "3.0" void = "1.0.2" url = { version = "2.5", optional = true, features = ["serde"] } [build-dependencies] log = { version = "0.4", features = ["std"] } bindgen = {version = "0.69", optional = true, default-features = false} regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]} walkdir = "2.1.4" toml = {version = "0.5", optional = true, default-features = false} mozbuild = {version = "0.1", optional = true} ================================================ FILE: style/README.md ================================================ servo-style =========== Style system for Servo, using [rust-cssparser](https://github.com/servo/rust-cssparser) for parsing. * [Documentation](https://book.servo.org/architecture/style.html). ================================================ FILE: style/applicable_declarations.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Applicable declarations management. use crate::derives::*; use crate::properties::PropertyDeclarationBlock; use crate::rule_tree::{CascadeLevel, RuleCascadeFlags, StyleSource}; use crate::shared_lock::Locked; use crate::stylesheets::layer_rule::LayerOrder; use servo_arc::Arc; use smallvec::SmallVec; /// List of applicable declarations. This is a transient structure that shuttles /// declarations between selector matching and inserting into the rule tree, and /// therefore we want to avoid heap-allocation where possible. /// /// In measurements on wikipedia, we pretty much never have more than 8 applicable /// declarations, so we could consider making this 8 entries instead of 16. /// However, it may depend a lot on workload, and stack space is cheap. pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>; /// Blink uses 18 bits to store source order, and does not check overflow [1]. /// That's a limit that could be reached in realistic webpages, so we use /// 24 bits and enforce defined behavior in the overflow case. /// /// Note that right now this restriction could be lifted if wanted (because we /// no longer stash the cascade level in the remaining bits), but we keep it in /// place in case we come up with a use-case for them, lacking reports of the /// current limit being too small. /// /// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/ /// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3 const SOURCE_ORDER_BITS: usize = 24; const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1; const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX; /// The cascade-level+layer order of this declaration. #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] pub struct CascadePriority { cascade_level: CascadeLevel, flags: RuleCascadeFlags, layer_order: LayerOrder, } const_assert_eq!( std::mem::size_of::(), std::mem::size_of::() ); impl PartialOrd for CascadePriority { #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for CascadePriority { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.cascade_level.cmp(&other.cascade_level).then_with(|| { let ordering = self.layer_order.cmp(&other.layer_order); if ordering == std::cmp::Ordering::Equal { return ordering; } // https://drafts.csswg.org/css-cascade-5/#cascade-layering // // Cascade layers (like declarations) are ordered by order // of appearance. When comparing declarations that belong to // different layers, then for normal rules the declaration // whose cascade layer is last wins, and for important rules // the declaration whose cascade layer is first wins. // // But the style attribute layer for some reason is special. if self.cascade_level.is_important() && !self.layer_order.is_style_attribute_layer() && !other.layer_order.is_style_attribute_layer() { ordering.reverse() } else { ordering } }) } } /// The kind of revert that we're applying. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RevertKind { /// revert Origin, /// revert-layer Layer, /// revert-rule Rule, } impl CascadePriority { /// Construct a new CascadePriority for a given (level, order, flags) triple. pub fn new( cascade_level: CascadeLevel, layer_order: LayerOrder, flags: RuleCascadeFlags, ) -> Self { Self { cascade_level, flags, layer_order, } } /// Returns the flags. #[inline] pub fn flags(&self) -> RuleCascadeFlags { self.flags } /// Set given flags. #[inline] pub fn set_flags(&mut self, flags: RuleCascadeFlags) { self.flags.insert(flags); } /// Returns the layer order. #[inline] pub fn layer_order(&self) -> LayerOrder { self.layer_order } /// Returns the cascade level. #[inline] pub fn cascade_level(&self) -> CascadeLevel { self.cascade_level } /// Whether this declaration should be allowed if `revert` / `revert-layer` / `revert-rule` /// have been specified on a given origin. /// /// `self` is the priority at which the revert has been specified. pub fn allows_when_reverted(&self, other: &Self, kind: RevertKind) -> bool { match kind { RevertKind::Origin => { other.cascade_level.origin().origin() < self.cascade_level.origin().origin() }, RevertKind::Layer => other.unimportant() < self.unimportant(), // Any other declaration for the same property we apply in the cascade needs to come // from another rule effectively. RevertKind::Rule => true, } } /// Convert this priority from "important" to "non-important", if needed. pub fn unimportant(&self) -> Self { Self { cascade_level: self.cascade_level.unimportant(), flags: self.flags, layer_order: self.layer_order, } } /// Convert this priority from "non-important" to "important", if needed. pub fn important(&self) -> Self { Self { cascade_level: self.cascade_level.important(), flags: self.flags, layer_order: self.layer_order, } } /// The same tree, in author origin, at the root layer. pub fn same_tree_author_normal_at_root_layer() -> Self { Self::new( CascadeLevel::same_tree_author_normal(), LayerOrder::root(), RuleCascadeFlags::empty(), ) } } /// Proximity to the scope root. /// /// https://drafts.csswg.org/css-cascade-6/#cascade-proximity #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] pub struct ScopeProximity(u16); impl PartialOrd for ScopeProximity { #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for ScopeProximity { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // Lower proximity to scope root wins other.0.cmp(&self.0) } } /// Sacrifice the largest possible value for infinity. This makes the comparison /// trivial. const PROXIMITY_INFINITY: u16 = u16::MAX; impl ScopeProximity { /// Construct a new scope proximity. pub fn new(proximity: usize) -> Self { if cfg!(debug_assertions) && proximity >= PROXIMITY_INFINITY as usize { warn!("Proximity out of bounds"); } Self(proximity.clamp(0, (PROXIMITY_INFINITY - 1) as usize) as u16) } /// Create a scope proximity for delcarations outside of any scope root. pub fn infinity() -> Self { Self(PROXIMITY_INFINITY) } /// If the proximity is finite, get the value. pub fn get(&self) -> Option { (self.0 != PROXIMITY_INFINITY).then(|| self.0) } } /// A property declaration together with its precedence among rules of equal /// specificity so that we can sort them. /// /// This represents the declarations in a given declaration block for a given /// importance. #[derive(Clone, Debug, MallocSizeOf, PartialEq)] pub struct ApplicableDeclarationBlock { /// The style source, either a style rule, or a property declaration block. #[ignore_malloc_size_of = "Arc"] pub source: StyleSource, /// Order of appearance in which this rule appears - Set to 0 if not relevant /// (e.g. Declaration from `style="/*...*/"`, presentation hints, animations /// - See `CascadePriority` instead). source_order: u32, /// The specificity of the selector. pub specificity: u32, /// The proximity to the scope root. pub scope_proximity: ScopeProximity, /// The cascade priority of the rule. pub cascade_priority: CascadePriority, } impl ApplicableDeclarationBlock { /// Constructs an applicable declaration block from a given property /// declaration block and importance. #[inline] pub fn from_declarations( declarations: Arc>, level: CascadeLevel, layer_order: LayerOrder, ) -> Self { ApplicableDeclarationBlock { source: StyleSource::from_declarations(declarations), source_order: 0, specificity: 0, scope_proximity: ScopeProximity::infinity(), cascade_priority: CascadePriority::new(level, layer_order, RuleCascadeFlags::empty()), } } /// Constructs an applicable declaration block from the given components. #[inline] pub fn new( source: StyleSource, source_order: u32, level: CascadeLevel, specificity: u32, layer_order: LayerOrder, scope_proximity: ScopeProximity, flags: RuleCascadeFlags, ) -> Self { ApplicableDeclarationBlock { source, source_order: source_order & SOURCE_ORDER_MASK, specificity, scope_proximity, cascade_priority: CascadePriority::new(level, layer_order, flags), } } /// Returns the source order of the block. #[inline] pub fn source_order(&self) -> u32 { self.source_order } /// Returns the cascade level of the block. #[inline] pub fn level(&self) -> CascadeLevel { self.cascade_priority.cascade_level() } /// Returns the cascade level of the block. #[inline] pub fn layer_order(&self) -> LayerOrder { self.cascade_priority.layer_order() } /// Returns the scope proximity of the block. #[inline] pub fn scope_proximity(&self) -> ScopeProximity { self.scope_proximity } /// Convenience method to consume self and return the right thing for the /// rule tree to iterate over. #[inline] pub fn for_rule_tree(self) -> (StyleSource, CascadePriority) { (self.source, self.cascade_priority) } /// Return the key used to sort applicable declarations. #[inline] pub fn sort_key(&self) -> (LayerOrder, u32, ScopeProximity, u32) { ( self.layer_order(), self.specificity, self.scope_proximity(), self.source_order(), ) } } // Size of this struct determines sorting and selector-matching performance. size_of_test!(ApplicableDeclarationBlock, 24); ================================================ FILE: style/author_styles.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! A set of author stylesheets and their computed representation, such as the //! ones used for ShadowRoot. use crate::derives::*; use crate::invalidation::stylesheets::StylesheetInvalidationSet; use crate::shared_lock::SharedRwLockReadGuard; use crate::stylesheet_set::AuthorStylesheetSet; use crate::stylesheets::StylesheetInDocument; use crate::stylist::CascadeData; use crate::stylist::Stylist; use servo_arc::Arc; use std::sync::LazyLock; /// A set of author stylesheets and their computed representation, such as the /// ones used for ShadowRoot. #[derive(MallocSizeOf)] pub struct GenericAuthorStyles where S: StylesheetInDocument + PartialEq + 'static, { /// The sheet collection, which holds the sheet pointers, the invalidations, /// and all that stuff. pub stylesheets: AuthorStylesheetSet, /// The actual cascade data computed from the stylesheets. #[ignore_malloc_size_of = "Measured as part of the stylist"] pub data: Arc, } pub use self::GenericAuthorStyles as AuthorStyles; static EMPTY_CASCADE_DATA: LazyLock> = LazyLock::new(|| Arc::new_leaked(CascadeData::new())); impl GenericAuthorStyles where S: StylesheetInDocument + PartialEq + 'static, { /// Create an empty AuthorStyles. #[inline] pub fn new() -> Self { Self { stylesheets: AuthorStylesheetSet::new(), data: EMPTY_CASCADE_DATA.clone(), } } /// Flush the pending sheet changes, updating `data` as appropriate. #[inline] pub fn flush( &mut self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard, ) -> StylesheetInvalidationSet { let (flusher, mut invalidations) = self.stylesheets.flush(); let result = stylist.rebuild_author_data( &self.data, flusher.sheets, guard, &mut invalidations.cascade_data_difference, ); if let Ok(Some(new_data)) = result { self.data = new_data; } invalidations } } ================================================ FILE: style/bezier.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Parametric Bézier curves. //! //! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit. #![deny(missing_docs)] use crate::values::CSSFloat; const NEWTON_METHOD_ITERATIONS: u8 = 8; /// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations. pub struct Bezier { ax: f64, bx: f64, cx: f64, ay: f64, by: f64, cy: f64, } impl Bezier { /// Calculate the output of a unit cubic Bézier curve from the two middle control points. /// /// X coordinate is time, Y coordinate is function advancement. /// The nominal range for both is 0 to 1. /// /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation /// starts at 0% and ends at 100%. pub fn calculate_bezier_output( progress: f64, epsilon: f64, x1: f32, y1: f32, x2: f32, y2: f32, ) -> f64 { // Check for a linear curve. if x1 == y1 && x2 == y2 { return progress; } // Ensure that we return 0 or 1 on both edges. if progress == 0.0 { return 0.0; } if progress == 1.0 { return 1.0; } // For negative values, try to extrapolate with tangent (p1 - p0) or, // if p1 is coincident with p0, with (p2 - p0). if progress < 0.0 { if x1 > 0.0 { return progress * y1 as f64 / x1 as f64; } if y1 == 0.0 && x2 > 0.0 { return progress * y2 as f64 / x2 as f64; } // If we can't calculate a sensible tangent, don't extrapolate at all. return 0.0; } // For values greater than 1, try to extrapolate with tangent (p2 - p3) or, // if p2 is coincident with p3, with (p1 - p3). if progress > 1.0 { if x2 < 1.0 { return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0); } if y2 == 1.0 && x1 < 1.0 { return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0); } // If we can't calculate a sensible tangent, don't extrapolate at all. return 1.0; } Bezier::new(x1, y1, x2, y2).solve(progress, epsilon) } #[inline] fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier { let cx = 3. * x1 as f64; let bx = 3. * (x2 as f64 - x1 as f64) - cx; let cy = 3. * y1 as f64; let by = 3. * (y2 as f64 - y1 as f64) - cy; Bezier { ax: 1.0 - cx - bx, bx: bx, cx: cx, ay: 1.0 - cy - by, by: by, cy: cy, } } #[inline] fn sample_curve_x(&self, t: f64) -> f64 { // ax * t^3 + bx * t^2 + cx * t ((self.ax * t + self.bx) * t + self.cx) * t } #[inline] fn sample_curve_y(&self, t: f64) -> f64 { ((self.ay * t + self.by) * t + self.cy) * t } #[inline] fn sample_curve_derivative_x(&self, t: f64) -> f64 { (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx } #[inline] fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 { // Fast path: Use Newton's method. let mut t = x; for _ in 0..NEWTON_METHOD_ITERATIONS { let x2 = self.sample_curve_x(t); if x2.approx_eq(x, epsilon) { return t; } let dx = self.sample_curve_derivative_x(t); if dx.approx_eq(0.0, 1e-6) { break; } t -= (x2 - x) / dx; } // Slow path: Use bisection. let (mut lo, mut hi, mut t) = (0.0, 1.0, x); if t < lo { return lo; } if t > hi { return hi; } while lo < hi { let x2 = self.sample_curve_x(t); if x2.approx_eq(x, epsilon) { return t; } if x > x2 { lo = t } else { hi = t } t = (hi - lo) / 2.0 + lo } t } /// Solve the bezier curve for a given `x` and an `epsilon`, that should be /// between zero and one. #[inline] fn solve(&self, x: f64, epsilon: f64) -> f64 { self.sample_curve_y(self.solve_curve_x(x, epsilon)) } } trait ApproxEq { fn approx_eq(self, value: Self, epsilon: Self) -> bool; } impl ApproxEq for f64 { #[inline] fn approx_eq(self, value: f64, epsilon: f64) -> bool { (self - value).abs() < epsilon } } ================================================ FILE: style/bloom.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The style bloom filter is used as an optimization when matching deep //! descendant selectors. #![deny(missing_docs)] use crate::dom::{SendElement, TElement}; use crate::LocalName; use atomic_refcell::{AtomicRefCell, AtomicRefMut}; use selectors::bloom::BloomFilter; use smallvec::SmallVec; thread_local! { /// Bloom filters are large allocations, so we store them in thread-local storage /// such that they can be reused across style traversals. StyleBloom is responsible /// for ensuring that the bloom filter is zeroed when it is dropped. /// /// We intentionally leak this from TLS because we don't have the guarantee /// of TLS destructors to run in worker threads. /// /// Also, leaking it guarantees that we can borrow it indefinitely. /// /// We could change this once https://github.com/rayon-rs/rayon/issues/688 /// is fixed, hopefully, which point we'd need to change the filter member below to be an /// arc and carry an owning reference around or so. static BLOOM_KEY: &'static AtomicRefCell = Box::leak(Default::default()); } /// A struct that allows us to fast-reject deep descendant selectors avoiding /// selector-matching. /// /// This is implemented using a counting bloom filter, and it's a standard /// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's /// `SelectorFilter`. /// /// The constraints for Servo's style system are a bit different compared to /// traditional style systems given Servo does a parallel breadth-first /// traversal instead of a sequential depth-first traversal. /// /// This implies that we need to track a bit more state than other browsers to /// ensure we're doing the correct thing during the traversal, and being able to /// apply this optimization effectively. /// /// Concretely, we have a bloom filter instance per worker thread, and we track /// the current DOM depth in order to find a common ancestor when it doesn't /// match the previous element we've styled. /// /// This is usually a pretty fast operation (we use to be one level deeper than /// the previous one), but in the case of work-stealing, we may needed to push /// and pop multiple elements. /// /// See the `insert_parents_recovering`, where most of the magic happens. /// /// Regarding thread-safety, this struct is safe because: /// /// * We clear this after a restyle. /// * The DOM shape and attributes (and every other thing we access here) are /// immutable during a restyle. /// pub struct StyleBloom { /// A handle to the bloom filter from the thread upon which this StyleBloom /// was created. We use AtomicRefCell so that this is all |Send|, which allows /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the /// parent thread. filter: AtomicRefMut<'static, BloomFilter>, /// The stack of elements that this bloom filter contains, along with the /// number of hashes pushed for each element. elements: SmallVec<[PushedElement; 16]>, /// Stack of hashes that have been pushed onto this filter. pushed_hashes: SmallVec<[u32; 64]>, } /// The very rough benchmarks in the selectors crate show clear() /// costing about 25 times more than remove_hash(). We use this to implement /// clear() more efficiently when only a small number of hashes have been /// pushed. /// /// One subtly to note is that remove_hash() will not touch the value /// if the filter overflowed. However, overflow can only occur if we /// get 255 collisions on the same hash value, and 25 < 255. const MEMSET_CLEAR_THRESHOLD: usize = 25; struct PushedElement { /// The element that was pushed. element: SendElement, /// The number of hashes pushed for the element. num_hashes: usize, } impl PushedElement { fn new(el: E, num_hashes: usize) -> Self { PushedElement { element: unsafe { SendElement::new(el) }, num_hashes, } } } /// Returns whether the attribute name is excluded from the bloom filter. /// /// We do this for attributes that are very common but not commonly used in /// selectors. #[inline] pub fn is_attr_name_excluded_from_filter(name: &LocalName) -> bool { *name == local_name!("class") || *name == local_name!("id") || *name == local_name!("style") } /// Gather all relevant hash for fast-reject filters from an element. pub fn each_relevant_element_hash(element: E, mut f: F) where E: TElement, F: FnMut(u32), { f(element.local_name().get_hash()); f(element.namespace().get_hash()); if let Some(id) = element.id() { f(id.get_hash()); } element.each_class(|class| f(class.get_hash())); element.each_attr_name(|name| { if !is_attr_name_excluded_from_filter(name) { f(name.get_hash()) } }); } impl Drop for StyleBloom { fn drop(&mut self) { // Leave the reusable bloom filter in a zeroed state. self.clear(); } } impl StyleBloom { /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread- /// local filter buffer, creating multiple live StyleBloom instances at /// the same time on the same thread will panic. // Forced out of line to limit stack frame sizes after extra inlining from // https://github.com/rust-lang/rust/pull/43931 // // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 #[inline(never)] pub fn new() -> Self { let filter = BLOOM_KEY.with(|b| b.borrow_mut()); debug_assert!( filter.is_zeroed(), "Forgot to zero the bloom filter last time" ); StyleBloom { filter, elements: Default::default(), pushed_hashes: Default::default(), } } /// Return the bloom filter used properly by the `selectors` crate. pub fn filter(&self) -> &BloomFilter { &*self.filter } /// Push an element to the bloom filter, knowing that it's a child of the /// last element parent. pub fn push(&mut self, element: E) { if cfg!(debug_assertions) { if self.elements.is_empty() { assert!(element.traversal_parent().is_none()); } } self.push_internal(element); } /// Same as `push`, but without asserting, in order to use it from /// `rebuild`. fn push_internal(&mut self, element: E) { let mut count = 0; each_relevant_element_hash(element, |hash| { count += 1; self.filter.insert_hash(hash); self.pushed_hashes.push(hash); }); self.elements.push(PushedElement::new(element, count)); } /// Pop the last element in the bloom filter and return it. #[inline] fn pop(&mut self) -> Option { let PushedElement { element, num_hashes, } = self.elements.pop()?; let popped_element = *element; // Verify that the pushed hashes match the ones we'd get from the element. let mut expected_hashes = vec![]; if cfg!(debug_assertions) { each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash)); } for _ in 0..num_hashes { let hash = self.pushed_hashes.pop().unwrap(); debug_assert_eq!(expected_hashes.pop().unwrap(), hash); self.filter.remove_hash(hash); } Some(popped_element) } /// Returns the DOM depth of elements that can be correctly /// matched against the bloom filter (that is, the number of /// elements in our list). pub fn matching_depth(&self) -> usize { self.elements.len() } /// Clears the bloom filter. pub fn clear(&mut self) { self.elements.clear(); if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD { self.filter.clear(); self.pushed_hashes.clear(); } else { for hash in self.pushed_hashes.drain(..) { self.filter.remove_hash(hash); } debug_assert!(self.filter.is_zeroed()); } } /// Rebuilds the bloom filter up to the parent of the given element. pub fn rebuild(&mut self, mut element: E) { self.clear(); let mut parents_to_insert = SmallVec::<[E; 16]>::new(); while let Some(parent) = element.traversal_parent() { parents_to_insert.push(parent); element = parent; } for parent in parents_to_insert.drain(..).rev() { self.push(parent); } } /// In debug builds, asserts that all the parents of `element` are in the /// bloom filter. /// /// Goes away in release builds. pub fn assert_complete(&self, mut element: E) { if cfg!(debug_assertions) { let mut checked = 0; while let Some(parent) = element.traversal_parent() { assert_eq!( parent, *(self.elements[self.elements.len() - 1 - checked].element) ); element = parent; checked += 1; } assert_eq!(checked, self.elements.len()); } } /// Get the element that represents the chain of things inserted /// into the filter right now. That chain is the given element /// (if any) and its ancestors. #[inline] pub fn current_parent(&self) -> Option { self.elements.last().map(|ref el| *el.element) } /// Insert the parents of an element in the bloom filter, trying to recover /// the filter if the last element inserted doesn't match. /// /// Gets the element depth in the dom, to make it efficient, or if not /// provided always rebuilds the filter from scratch. /// /// Returns the new bloom filter depth, that the traversal code is /// responsible to keep around if it wants to get an effective filter. pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) { // Easy case, we're in a different restyle, or we're empty. if self.elements.is_empty() { self.rebuild(element); return; } let traversal_parent = match element.traversal_parent() { Some(parent) => parent, None => { // Yay, another easy case. self.clear(); return; }, }; if self.current_parent() == Some(traversal_parent) { // Ta da, cache hit, we're all done. return; } if element_depth == 0 { self.clear(); return; } // We should've early exited above. debug_assert!( element_depth != 0, "We should have already cleared the bloom filter" ); debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!"); // Now the fun begins: We have the depth of the dom and the depth of the // last element inserted in the filter, let's try to find a common // parent. // // The current depth, that is, the depth of the last element inserted in // the bloom filter, is the number of elements _minus one_, that is: if // there's one element, it must be the root -> depth zero. let mut current_depth = self.elements.len() - 1; // If the filter represents an element too deep in the dom, we need to // pop ancestors. while current_depth > element_depth - 1 { self.pop().expect("Emilio is bad at math"); current_depth -= 1; } // Now let's try to find a common parent in the bloom filter chain, // starting with traversal_parent. let mut common_parent = traversal_parent; let mut common_parent_depth = element_depth - 1; // Let's collect the parents we are going to need to insert once we've // found the common one. let mut parents_to_insert = SmallVec::<[E; 16]>::new(); // If the bloom filter still doesn't have enough elements, the common // parent is up in the dom. while common_parent_depth > current_depth { // TODO(emilio): Seems like we could insert parents here, then // reverse the slice. parents_to_insert.push(common_parent); common_parent = common_parent.traversal_parent().expect("We were lied to"); common_parent_depth -= 1; } // Now the two depths are the same. debug_assert_eq!(common_parent_depth, current_depth); // Happy case: The parents match, we only need to push the ancestors // we've collected and we'll never enter in this loop. // // Not-so-happy case: Parent's don't match, so we need to keep going up // until we find a common ancestor. // // Gecko currently models native anonymous content that conceptually // hangs off the document (such as scrollbars) as a separate subtree // from the document root. // // Thus it's possible with Gecko that we do not find any common // ancestor. while *(self.elements.last().unwrap().element) != common_parent { parents_to_insert.push(common_parent); self.pop().unwrap(); common_parent = match common_parent.traversal_parent() { Some(parent) => parent, None => { debug_assert!(self.elements.is_empty()); if cfg!(feature = "gecko") { break; } else { panic!("should have found a common ancestor"); } }, } } // Now the parents match, so insert the stack of elements we have been // collecting so far. for parent in parents_to_insert.drain(..).rev() { self.push(parent); } debug_assert_eq!(self.elements.len(), element_depth); // We're done! Easy. } } ================================================ FILE: style/build.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::env; use std::path::Path; use std::process::{exit, Command}; use std::sync::LazyLock; use walkdir::WalkDir; #[cfg(feature = "gecko")] mod build_gecko; #[cfg(not(feature = "gecko"))] mod build_gecko { pub fn generate() {} } pub static PYTHON: LazyLock = LazyLock::new(|| { env::var("PYTHON3").ok().unwrap_or_else(|| { let candidates = if cfg!(windows) { ["python.exe"] } else { ["python3"] }; for &name in &candidates { if Command::new(name) .arg("--version") .output() .ok() .map_or(false, |out| out.status.success()) { return name.to_owned(); } } panic!( "Can't find python (tried {})! Try fixing PATH or setting the PYTHON3 env var", candidates.join(", ") ) }) }); fn generate_properties(engine: &str) { for entry in WalkDir::new("properties") { let entry = entry.unwrap(); match entry.path().extension().and_then(|e| e.to_str()) { Some("mako") | Some("rs") | Some("py") | Some("zip") | Some("toml") => { println!("cargo:rerun-if-changed={}", entry.path().display()); }, _ => {}, } } let script = Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) .join("properties") .join("build.py"); let status = Command::new(&*PYTHON) // `cargo publish` isn't happy with the `__pycache__` files that are created // when we run the property generator. // // TODO(mrobinson): Is this happening because of how we run this script? It // would be better to ensure are just placed in the output directory. .env("PYTHONDONTWRITEBYTECODE", "1") .arg(&script) .arg(engine) .arg("style-crate") .status() .unwrap(); if !status.success() { exit(1) } } fn main() { let gecko = cfg!(feature = "gecko"); let servo = cfg!(feature = "servo"); let engine = match (gecko, servo) { (true, false) => "gecko", (false, true) => "servo", _ => panic!( "\n\n\ The style crate requires enabling one of its 'servo' or 'gecko' feature flags. \ \n\n" ), }; println!("cargo:rerun-if-changed=build.rs"); println!("cargo:out_dir={}", env::var("OUT_DIR").unwrap()); generate_properties(engine); build_gecko::generate(); } ================================================ FILE: style/build_gecko.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use super::PYTHON; use bindgen::{Builder, CodegenConfig}; use regex::Regex; use std::cmp; use std::collections::HashSet; use std::env; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{exit, Command}; use std::slice; use std::sync::LazyLock; use std::sync::Mutex; use std::time::SystemTime; use toml; use toml::value::Table; static OUTDIR_PATH: LazyLock = LazyLock::new(|| PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko")); const STRUCTS_FILE: &'static str = "structs.rs"; fn read_config(path: &PathBuf) -> Table { println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); update_last_modified(&path); let mut contents = String::new(); File::open(path) .expect("Failed to open config file") .read_to_string(&mut contents) .expect("Failed to read config file"); match toml::from_str::(&contents) { Ok(result) => result, Err(e) => panic!("Failed to parse config file: {}", e), } } static CONFIG: LazyLock
= LazyLock::new(|| { // Load Gecko's binding generator config from the source tree. let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml"); read_config(&path) }); static BINDGEN_FLAGS: LazyLock> = LazyLock::new(|| { mozbuild::config::BINDGEN_SYSTEM_FLAGS .iter() .chain(&mozbuild::config::NSPR_CFLAGS) .chain(&mozbuild::config::MOZ_PIXMAN_CFLAGS) .chain(&mozbuild::config::MOZ_ICU_CFLAGS) .map(|s| s.to_string()) .collect() }); static INCLUDE_RE: LazyLock = LazyLock::new(|| Regex::new(r#"#include\s*"(.+?)""#).unwrap()); static DISTDIR_PATH: LazyLock = LazyLock::new(|| mozbuild::TOPOBJDIR.join("dist")); static SEARCH_PATHS: LazyLock> = LazyLock::new(|| { vec![ DISTDIR_PATH.join("include"), DISTDIR_PATH.join("include/nspr"), ] }); static ADDED_PATHS: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); static LAST_MODIFIED: LazyLock> = LazyLock::new(|| { Mutex::new( get_modified_time(&env::current_exe().unwrap()) .expect("Failed to get modified time of executable"), ) }); fn get_modified_time(file: &Path) -> Option { file.metadata().and_then(|m| m.modified()).ok() } fn update_last_modified(file: &Path) { let modified = get_modified_time(file).expect("Couldn't get file modification time"); let mut last_modified = LAST_MODIFIED.lock().unwrap(); *last_modified = cmp::max(modified, *last_modified); } fn search_include(name: &str) -> Option { for path in SEARCH_PATHS.iter() { let file = path.join(name); if file.is_file() { update_last_modified(&file); return Some(file); } } None } fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet) { if added_paths.contains(&path) { return; } let mut file = File::open(&path).unwrap(); let mut content = String::new(); file.read_to_string(&mut content).unwrap(); added_paths.insert(path); // Find all includes and add them recursively for cap in INCLUDE_RE.captures_iter(&content) { if let Some(path) = search_include(cap.get(1).unwrap().as_str()) { add_headers_recursively(path, added_paths); } } } fn add_include(name: &str) -> String { let mut added_paths = ADDED_PATHS.lock().unwrap(); let file = match search_include(name) { Some(file) => file, None => panic!("Include not found: {}", name), }; let result = String::from(file.to_str().unwrap()); add_headers_recursively(file, &mut *added_paths); result } trait BuilderExt { fn get_initial_builder() -> Builder; fn include>(self, file: T) -> Builder; } impl BuilderExt for Builder { fn get_initial_builder() -> Builder { // Disable rust unions, because we replace some types inside of // them. let mut builder = Builder::default() .size_t_is_usize(true) .disable_untagged_union(); let rustfmt_path = env::var_os("RUSTFMT") .filter(|p| !p.is_empty()) .map(PathBuf::from); if let Some(path) = rustfmt_path { builder = builder.with_rustfmt(path); } for dir in SEARCH_PATHS.iter() { builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap()); } builder = builder.include(add_include("mozilla-config.h")); if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() { builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1"); } for item in &*BINDGEN_FLAGS { builder = builder.clang_arg(item); } builder } fn include>(self, file: T) -> Builder { self.clang_arg("-include").clang_arg(file) } } struct Fixup { pat: String, rep: String, } fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) { let out_file = OUTDIR_PATH.join(file); if let Some(modified) = get_modified_time(&out_file) { // Don't generate the file if nothing it depends on was modified. let last_modified = LAST_MODIFIED.lock().unwrap(); if *last_modified <= modified { return; } } let command_line_opts = builder.command_line_flags(); let result = builder.generate(); let mut result = match result { Ok(bindings) => bindings.to_string(), Err(_) => { panic!( "Failed to generate bindings, flags: {:?}", command_line_opts ); }, }; for fixup in fixups.iter() { result = Regex::new(&fixup.pat) .unwrap() .replace_all(&result, &*fixup.rep) .into_owned() .into(); } let bytes = result.into_bytes(); File::create(&out_file) .unwrap() .write_all(&bytes) .expect("Unable to write output"); } struct BuilderWithConfig<'a> { builder: Builder, config: &'a Table, used_keys: HashSet<&'static str>, } impl<'a> BuilderWithConfig<'a> { fn new(builder: Builder, config: &'a Table) -> Self { BuilderWithConfig { builder, config, used_keys: HashSet::new(), } } fn handle_list(self, key: &'static str, func: F) -> BuilderWithConfig<'a> where F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder, { let mut builder = self.builder; let config = self.config; let mut used_keys = self.used_keys; if let Some(list) = config.get(key) { used_keys.insert(key); builder = func(builder, list.as_array().unwrap().as_slice().iter()); } BuilderWithConfig { builder, config, used_keys, } } fn handle_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> where F: FnMut(Builder, &'a toml::Value) -> Builder, { self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item))) } fn handle_str_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> where F: FnMut(Builder, &'a str) -> Builder, { self.handle_items(key, |b, item| func(b, item.as_str().unwrap())) } fn handle_table_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> where F: FnMut(Builder, &'a Table) -> Builder, { self.handle_items(key, |b, item| func(b, item.as_table().unwrap())) } fn handle_common(self, fixups: &mut Vec) -> BuilderWithConfig<'a> { self.handle_str_items("headers", |b, item| b.header(add_include(item))) .handle_str_items("raw-lines", |b, item| b.raw_line(item)) .handle_str_items("hide-types", |b, item| b.blocklist_type(item)) .handle_table_items("fixups", |builder, item| { fixups.push(Fixup { pat: item["pat"].as_str().unwrap().into(), rep: item["rep"].as_str().unwrap().into(), }); builder }) } fn get_builder(self) -> Builder { for key in self.config.keys() { if !self.used_keys.contains(key.as_str()) { panic!("Unknown key: {}", key); } } self.builder } } fn generate_structs() { let builder = Builder::get_initial_builder() .enable_cxx_namespaces() .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS); let mut fixups = vec![]; let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap()) .handle_common(&mut fixups) .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item)) .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item)) .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item)) .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item)) .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item)) .handle_str_items("opaque-types", |b, item| b.opaque_type(item)) .handle_table_items("cbindgen-types", |b, item| { let gecko = item["gecko"].as_str().unwrap(); let servo = item["servo"].as_str().unwrap(); b.blocklist_type(format!("mozilla::{}", gecko)) .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko)) }) .handle_table_items("mapped-generic-types", |builder, item| { let generic = item["generic"].as_bool().unwrap(); let gecko = item["gecko"].as_str().unwrap(); let servo = item["servo"].as_str().unwrap(); let gecko_name = gecko.rsplit("::").next().unwrap(); let gecko = gecko .split("::") .map(|s| format!("\\s*{}\\s*", s)) .collect::>() .join("::"); fixups.push(Fixup { pat: format!("\\broot\\s*::\\s*{}\\b", gecko), rep: format!("crate::gecko_bindings::structs::{}", gecko_name), }); builder.blocklist_type(gecko).raw_line(format!( "pub type {0}{2} = {1}{2};", gecko_name, servo, if generic { "" } else { "" } )) }) .get_builder(); write_binding_file(builder, STRUCTS_FILE, &fixups); } fn setup_logging() -> bool { struct BuildLogger { file: Option>, filter: String, } impl log::Log for BuildLogger { fn enabled(&self, meta: &log::Metadata) -> bool { self.file.is_some() && meta.target().contains(&self.filter) } fn log(&self, record: &log::Record) { if !self.enabled(record.metadata()) { return; } let mut file = self.file.as_ref().unwrap().lock().unwrap(); let _ = writeln!( file, "{} - {} - {} @ {}:{}", record.level(), record.target(), record.args(), record.file().unwrap_or(""), record.line().unwrap_or(0) ); } fn flush(&self) { if let Some(ref file) = self.file { file.lock().unwrap().flush().unwrap(); } } } if let Some(path) = env::var_os("STYLO_BUILD_LOG") { log::set_max_level(log::LevelFilter::Debug); log::set_boxed_logger(Box::new(BuildLogger { file: fs::File::create(path).ok().map(Mutex::new), filter: env::var("STYLO_BUILD_FILTER") .ok() .unwrap_or_else(|| "bindgen".to_owned()), })) .expect("Failed to set logger."); true } else { false } } fn generate_atoms() { let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) .join("gecko") .join("regen_atoms.py"); println!("cargo:rerun-if-changed={}", script.display()); let status = Command::new(&*PYTHON) .arg(&script) .arg(DISTDIR_PATH.as_os_str()) .arg(OUTDIR_PATH.as_os_str()) .status() .unwrap(); if !status.success() { exit(1); } } pub fn generate() { println!("cargo:rerun-if-changed=build_gecko.rs"); fs::create_dir_all(&*OUTDIR_PATH).unwrap(); setup_logging(); generate_structs(); generate_atoms(); for path in ADDED_PATHS.lock().unwrap().iter() { println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); } } ================================================ FILE: style/color/color_function.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Output of parsing a color function, e.g. rgb(..), hsl(..), color(..) use std::fmt::Write; use super::{ component::ColorComponent, convert::normalize_hue, parsing::{NumberOrAngleComponent, NumberOrPercentageComponent}, AbsoluteColor, ColorFlags, ColorSpace, }; use crate::derives::*; use crate::values::{ computed::color::Color as ComputedColor, generics::Optional, normalize, specified::color::Color as SpecifiedColor, }; use cssparser::color::{clamp_floor_256_f32, OPAQUE}; /// Represents a specified color function. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] #[repr(u8)] pub enum ColorFunction { /// Rgb( Optional, // origin ColorComponent, // red ColorComponent, // green ColorComponent, // blue ColorComponent, // alpha ), /// Hsl( Optional, // origin ColorComponent, // hue ColorComponent, // saturation ColorComponent, // lightness ColorComponent, // alpha ), /// Hwb( Optional, // origin ColorComponent, // hue ColorComponent, // whiteness ColorComponent, // blackness ColorComponent, // alpha ), /// Lab( Optional, // origin ColorComponent, // lightness ColorComponent, // a ColorComponent, // b ColorComponent, // alpha ), /// Lch( Optional, // origin ColorComponent, // lightness ColorComponent, // chroma ColorComponent, // hue ColorComponent, // alpha ), /// Oklab( Optional, // origin ColorComponent, // lightness ColorComponent, // a ColorComponent, // b ColorComponent, // alpha ), /// Oklch( Optional, // origin ColorComponent, // lightness ColorComponent, // chroma ColorComponent, // hue ColorComponent, // alpha ), /// Color( Optional, // origin ColorComponent, // red / x ColorComponent, // green / y ColorComponent, // blue / z ColorComponent, // alpha ColorSpace, ), } impl ColorFunction { /// Try to resolve into a valid absolute color. pub fn resolve_to_absolute(&self) -> Result { macro_rules! alpha { ($alpha:expr, $origin_color:expr) => {{ $alpha .resolve($origin_color)? .map(|value| normalize(value.to_number(1.0)).clamp(0.0, OPAQUE)) }}; } Ok(match self { ColorFunction::Rgb(origin_color, r, g, b, alpha) => { // Use `color(srgb ...)` to serialize `rgb(...)` if an origin color is available; // this is the only reason for now. let use_color_syntax = origin_color.is_some(); if use_color_syntax { let origin_color = origin_color.as_ref().map(|origin| { let origin = origin.to_color_space(ColorSpace::Srgb); // Because rgb(..) syntax have components in range [0..255), we have to // map them. // NOTE: The IS_LEGACY_SRGB flag is not added back to the color, because // we're going to return the modern color(srgb ..) syntax. AbsoluteColor::new( ColorSpace::Srgb, origin.c0().map(|v| v * 255.0), origin.c1().map(|v| v * 255.0), origin.c2().map(|v| v * 255.0), origin.alpha(), ) }); // We have to map all the components back to [0..1) range after all the // calculations. AbsoluteColor::new( ColorSpace::Srgb, r.resolve(origin_color.as_ref())? .map(|c| c.to_number(255.0) / 255.0), g.resolve(origin_color.as_ref())? .map(|c| c.to_number(255.0) / 255.0), b.resolve(origin_color.as_ref())? .map(|c| c.to_number(255.0) / 255.0), alpha!(alpha, origin_color.as_ref()), ) } else { #[inline] fn resolve( component: &ColorComponent, origin_color: Option<&AbsoluteColor>, ) -> Result { Ok(clamp_floor_256_f32( component .resolve(origin_color)? .map_or(0.0, |value| value.to_number(u8::MAX as f32)), )) } let origin_color = origin_color.as_ref().map(|o| o.into_srgb_legacy()); AbsoluteColor::srgb_legacy( resolve(r, origin_color.as_ref())?, resolve(g, origin_color.as_ref())?, resolve(b, origin_color.as_ref())?, alpha!(alpha, origin_color.as_ref()).unwrap_or(0.0), ) } }, ColorFunction::Hsl(origin_color, h, s, l, alpha) => { // Percent reference range for S and L: 0% = 0.0, 100% = 100.0 const LIGHTNESS_RANGE: f32 = 100.0; const SATURATION_RANGE: f32 = 100.0; // If the origin color: // - was *NOT* specified, then we stick with the old way of serializing the // value to rgb(..). // - was specified, we don't use the rgb(..) syntax, because we should allow the // color to be out of gamut and not clamp. let use_rgb_sytax = origin_color.is_none(); let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Hsl)); let mut result = AbsoluteColor::new( ColorSpace::Hsl, h.resolve(origin_color.as_ref())? .map(|angle| normalize_hue(angle.degrees())), s.resolve(origin_color.as_ref())?.map(|s| { if use_rgb_sytax { s.to_number(SATURATION_RANGE).clamp(0.0, SATURATION_RANGE) } else { s.to_number(SATURATION_RANGE) } }), l.resolve(origin_color.as_ref())?.map(|l| { if use_rgb_sytax { l.to_number(LIGHTNESS_RANGE).clamp(0.0, LIGHTNESS_RANGE) } else { l.to_number(LIGHTNESS_RANGE) } }), alpha!(alpha, origin_color.as_ref()), ); if use_rgb_sytax { result.flags.insert(ColorFlags::IS_LEGACY_SRGB); } result }, ColorFunction::Hwb(origin_color, h, w, b, alpha) => { // If the origin color: // - was *NOT* specified, then we stick with the old way of serializing the // value to rgb(..). // - was specified, we don't use the rgb(..) syntax, because we should allow the // color to be out of gamut and not clamp. let use_rgb_sytax = origin_color.is_none(); // Percent reference range for W and B: 0% = 0.0, 100% = 100.0 const WHITENESS_RANGE: f32 = 100.0; const BLACKNESS_RANGE: f32 = 100.0; let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Hwb)); let mut result = AbsoluteColor::new( ColorSpace::Hwb, h.resolve(origin_color.as_ref())? .map(|angle| normalize_hue(angle.degrees())), w.resolve(origin_color.as_ref())?.map(|w| { if use_rgb_sytax { w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE) } else { w.to_number(WHITENESS_RANGE) } }), b.resolve(origin_color.as_ref())?.map(|b| { if use_rgb_sytax { b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE) } else { b.to_number(BLACKNESS_RANGE) } }), alpha!(alpha, origin_color.as_ref()), ); if use_rgb_sytax { result.flags.insert(ColorFlags::IS_LEGACY_SRGB); } result }, ColorFunction::Lab(origin_color, l, a, b, alpha) => { // for L: 0% = 0.0, 100% = 100.0 // for a and b: -100% = -125, 100% = 125 const LIGHTNESS_RANGE: f32 = 100.0; const A_B_RANGE: f32 = 125.0; let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Lab)); AbsoluteColor::new( ColorSpace::Lab, l.resolve(origin_color.as_ref())? .map(|l| l.to_number(LIGHTNESS_RANGE)), a.resolve(origin_color.as_ref())? .map(|a| a.to_number(A_B_RANGE)), b.resolve(origin_color.as_ref())? .map(|b| b.to_number(A_B_RANGE)), alpha!(alpha, origin_color.as_ref()), ) }, ColorFunction::Lch(origin_color, l, c, h, alpha) => { // for L: 0% = 0.0, 100% = 100.0 // for C: 0% = 0, 100% = 150 const LIGHTNESS_RANGE: f32 = 100.0; const CHROMA_RANGE: f32 = 150.0; let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Lch)); AbsoluteColor::new( ColorSpace::Lch, l.resolve(origin_color.as_ref())? .map(|l| l.to_number(LIGHTNESS_RANGE)), c.resolve(origin_color.as_ref())? .map(|c| c.to_number(CHROMA_RANGE)), h.resolve(origin_color.as_ref())? .map(|angle| normalize_hue(angle.degrees())), alpha!(alpha, origin_color.as_ref()), ) }, ColorFunction::Oklab(origin_color, l, a, b, alpha) => { // for L: 0% = 0.0, 100% = 1.0 // for a and b: -100% = -0.4, 100% = 0.4 const LIGHTNESS_RANGE: f32 = 1.0; const A_B_RANGE: f32 = 0.4; let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Oklab)); AbsoluteColor::new( ColorSpace::Oklab, l.resolve(origin_color.as_ref())? .map(|l| l.to_number(LIGHTNESS_RANGE)), a.resolve(origin_color.as_ref())? .map(|a| a.to_number(A_B_RANGE)), b.resolve(origin_color.as_ref())? .map(|b| b.to_number(A_B_RANGE)), alpha!(alpha, origin_color.as_ref()), ) }, ColorFunction::Oklch(origin_color, l, c, h, alpha) => { // for L: 0% = 0.0, 100% = 1.0 // for C: 0% = 0.0 100% = 0.4 const LIGHTNESS_RANGE: f32 = 1.0; const CHROMA_RANGE: f32 = 0.4; let origin_color = origin_color .as_ref() .map(|o| o.to_color_space(ColorSpace::Oklch)); AbsoluteColor::new( ColorSpace::Oklch, l.resolve(origin_color.as_ref())? .map(|l| l.to_number(LIGHTNESS_RANGE)), c.resolve(origin_color.as_ref())? .map(|c| c.to_number(CHROMA_RANGE)), h.resolve(origin_color.as_ref())? .map(|angle| normalize_hue(angle.degrees())), alpha!(alpha, origin_color.as_ref()), ) }, ColorFunction::Color(origin_color, r, g, b, alpha, color_space) => { let origin_color = origin_color.as_ref().map(|o| { let mut result = o.to_color_space(*color_space); // If the origin color was a `rgb(..)` function, we should // make sure it doesn't have the legacy flag any more so // that it is recognized as a `color(srgb ..)` function. result.flags.set(ColorFlags::IS_LEGACY_SRGB, false); result }); AbsoluteColor::new( *color_space, r.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)), g.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)), b.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)), alpha!(alpha, origin_color.as_ref()), ) }, }) } } impl ColorFunction { /// Return true if the color funciton has an origin color specified. pub fn has_origin_color(&self) -> bool { match self { Self::Rgb(origin_color, ..) | Self::Hsl(origin_color, ..) | Self::Hwb(origin_color, ..) | Self::Lab(origin_color, ..) | Self::Lch(origin_color, ..) | Self::Oklab(origin_color, ..) | Self::Oklch(origin_color, ..) | Self::Color(origin_color, ..) => origin_color.is_some(), } } /// Try to resolve the color function to an [`AbsoluteColor`] that does not /// contain any variables (currentcolor, color components, etc.). pub fn resolve_to_absolute(&self) -> Result { // Map the color function to one with an absolute origin color. self.map_origin_color(|o| o.resolve_to_absolute())? .resolve_to_absolute() } } impl ColorFunction { /// Map the origin color to another type. pub fn map_origin_color( &self, f: impl FnOnce(&Color) -> Result, ) -> Result, ()> { macro_rules! map { ($f:ident, $o:expr, $c0:expr, $c1:expr, $c2:expr, $alpha:expr) => {{ ColorFunction::$f( match $o.as_ref() { Some(c) => Some(f(c)?), None => None, } .into(), $c0.clone(), $c1.clone(), $c2.clone(), $alpha.clone(), ) }}; } Ok(match self { ColorFunction::Rgb(o, c0, c1, c2, alpha) => map!(Rgb, o, c0, c1, c2, alpha), ColorFunction::Hsl(o, c0, c1, c2, alpha) => map!(Hsl, o, c0, c1, c2, alpha), ColorFunction::Hwb(o, c0, c1, c2, alpha) => map!(Hwb, o, c0, c1, c2, alpha), ColorFunction::Lab(o, c0, c1, c2, alpha) => map!(Lab, o, c0, c1, c2, alpha), ColorFunction::Lch(o, c0, c1, c2, alpha) => map!(Lch, o, c0, c1, c2, alpha), ColorFunction::Oklab(o, c0, c1, c2, alpha) => map!(Oklab, o, c0, c1, c2, alpha), ColorFunction::Oklch(o, c0, c1, c2, alpha) => map!(Oklch, o, c0, c1, c2, alpha), ColorFunction::Color(o, c0, c1, c2, alpha, color_space) => ColorFunction::Color( match o.as_ref() { Some(c) => Some(f(c)?), None => None, } .into(), c0.clone(), c1.clone(), c2.clone(), alpha.clone(), color_space.clone(), ), }) } } impl ColorFunction { /// Resolve a computed color function to an absolute computed color. pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor { // Map the color function to one with an absolute origin color. let resolvable = self .map_origin_color(|o| Ok(o.resolve_to_absolute(current_color))) .unwrap(); match resolvable.resolve_to_absolute() { Ok(color) => color, Err(..) => { debug_assert!( false, "the color could not be resolved even with a currentcolor specified?" ); AbsoluteColor::TRANSPARENT_BLACK }, } } } impl style_traits::ToCss for ColorFunction { fn to_css(&self, dest: &mut style_traits::CssWriter) -> std::fmt::Result where W: std::fmt::Write, { let (origin_color, alpha) = match self { Self::Rgb(origin_color, _, _, _, alpha) => { dest.write_str("rgb(")?; (origin_color, alpha) }, Self::Hsl(origin_color, _, _, _, alpha) => { dest.write_str("hsl(")?; (origin_color, alpha) }, Self::Hwb(origin_color, _, _, _, alpha) => { dest.write_str("hwb(")?; (origin_color, alpha) }, Self::Lab(origin_color, _, _, _, alpha) => { dest.write_str("lab(")?; (origin_color, alpha) }, Self::Lch(origin_color, _, _, _, alpha) => { dest.write_str("lch(")?; (origin_color, alpha) }, Self::Oklab(origin_color, _, _, _, alpha) => { dest.write_str("oklab(")?; (origin_color, alpha) }, Self::Oklch(origin_color, _, _, _, alpha) => { dest.write_str("oklch(")?; (origin_color, alpha) }, Self::Color(origin_color, _, _, _, alpha, _) => { dest.write_str("color(")?; (origin_color, alpha) }, }; if let Optional::Some(origin_color) = origin_color { dest.write_str("from ")?; origin_color.to_css(dest)?; dest.write_str(" ")?; } let is_opaque = if let ColorComponent::Value(value) = *alpha { value.to_number(OPAQUE) == OPAQUE } else { false }; macro_rules! serialize_alpha { ($alpha_component:expr) => {{ if !is_opaque && !matches!($alpha_component, ColorComponent::AlphaOmitted) { dest.write_str(" / ")?; $alpha_component.to_css(dest)?; } }}; } macro_rules! serialize_components { ($c0:expr, $c1:expr, $c2:expr) => {{ debug_assert!(!matches!($c0, ColorComponent::AlphaOmitted)); debug_assert!(!matches!($c1, ColorComponent::AlphaOmitted)); debug_assert!(!matches!($c2, ColorComponent::AlphaOmitted)); $c0.to_css(dest)?; dest.write_str(" ")?; $c1.to_css(dest)?; dest.write_str(" ")?; $c2.to_css(dest)?; }}; } match self { Self::Rgb(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Hsl(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Hwb(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Lab(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Lch(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Oklab(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Oklch(_, c0, c1, c2, alpha) => { serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, Self::Color(_, c0, c1, c2, alpha, color_space) => { color_space.to_css(dest)?; dest.write_str(" ")?; serialize_components!(c0, c1, c2); serialize_alpha!(alpha); }, } dest.write_str(")") } } ================================================ FILE: style/color/component.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Parse/serialize and resolve a single color component. use std::fmt::Write; use super::{ parsing::{rcs_enabled, ChannelKeyword}, AbsoluteColor, }; use crate::derives::*; use crate::{ parser::ParserContext, values::{ animated::ToAnimatedValue, generics::calc::{CalcUnits, GenericCalcNode}, specified::calc::{AllowParse, Leaf}, }, }; use cssparser::{color::OPAQUE, Parser, Token}; use style_traits::{ParseError, ToCss}; /// A single color component. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[repr(u8)] pub enum ColorComponent { /// The "none" keyword. None, /// A absolute value. Value(ValueType), /// A channel keyword, e.g. `r`, `l`, `alpha`, etc. ChannelKeyword(ChannelKeyword), /// A calc() value. Calc(Box>), /// Used when alpha components are not specified. AlphaOmitted, } impl ColorComponent { /// Return true if the component is "none". #[inline] pub fn is_none(&self) -> bool { matches!(self, Self::None) } } /// An utility trait that allows the construction of [ColorComponent] /// `ValueType`'s after parsing a color component. pub trait ColorComponentType: Sized + Clone { // TODO(tlouw): This function should be named according to the rules in the spec // stating that all the values coming from color components are // numbers and that each has their own rules dependeing on types. /// Construct a new component from a single value. fn from_value(value: f32) -> Self; /// Return the [CalcUnits] flags that the impl can handle. fn units() -> CalcUnits; /// Try to create a new component from the given token. fn try_from_token(token: &Token) -> Result; /// Try to create a new component from the given [CalcNodeLeaf] that was /// resolved from a [CalcNode]. fn try_from_leaf(leaf: &Leaf) -> Result; } impl ColorComponent { /// Parse a single [ColorComponent]. pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result> { let location = input.current_source_location(); match *input.next()? { Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => { Ok(ColorComponent::None) }, ref t @ Token::Ident(ref ident) => { let Ok(channel_keyword) = ChannelKeyword::from_ident(ident) else { return Err(location.new_unexpected_token_error(t.clone())); }; Ok(ColorComponent::ChannelKeyword(channel_keyword)) }, Token::Function(ref name) => { let function = GenericCalcNode::math_function(context, name, location)?; let allow = AllowParse::new(if rcs_enabled() { ValueType::units() | CalcUnits::COLOR_COMPONENT } else { ValueType::units() }); let mut node = GenericCalcNode::parse(context, input, function, allow)?; // TODO(tlouw): We only have to simplify the node when we have to store it, but we // only know if we have to store it much later when the whole color // can't be resolved to absolute at which point the calc nodes are // burried deep in a [ColorFunction] struct. node.simplify_and_sort(); Ok(Self::Calc(Box::new(node))) }, ref t => ValueType::try_from_token(t) .map(Self::Value) .map_err(|_| location.new_unexpected_token_error(t.clone())), } } /// Resolve a [ColorComponent] into a float. None is "none". pub fn resolve(&self, origin_color: Option<&AbsoluteColor>) -> Result, ()> { Ok(match self { ColorComponent::None => None, ColorComponent::Value(value) => Some(value.clone()), ColorComponent::ChannelKeyword(channel_keyword) => match origin_color { Some(origin_color) => { let value = origin_color.get_component_by_channel_keyword(*channel_keyword)?; Some(ValueType::from_value(value.unwrap_or(0.0))) }, None => return Err(()), }, ColorComponent::Calc(node) => { let Ok(resolved_leaf) = node.resolve_map(|leaf| { Ok(match leaf { Leaf::ColorComponent(channel_keyword) => match origin_color { Some(origin_color) => { let value = origin_color .get_component_by_channel_keyword(*channel_keyword)?; Leaf::Number(value.unwrap_or(0.0)) }, None => return Err(()), }, l => l.clone(), }) }) else { return Err(()); }; Some(ValueType::try_from_leaf(&resolved_leaf)?) }, ColorComponent::AlphaOmitted => { if let Some(origin_color) = origin_color { // // If the alpha value of the relative color is omitted, it defaults to that of // the origin color (rather than defaulting to 100%, as it does in the absolute // syntax). origin_color.alpha().map(ValueType::from_value) } else { Some(ValueType::from_value(OPAQUE)) } }, }) } } impl ToCss for ColorComponent { fn to_css(&self, dest: &mut style_traits::CssWriter) -> std::fmt::Result where W: Write, { match self { ColorComponent::None => dest.write_str("none")?, ColorComponent::Value(value) => value.to_css(dest)?, ColorComponent::ChannelKeyword(channel_keyword) => channel_keyword.to_css(dest)?, ColorComponent::Calc(node) => { // When we only have a channel keyword in a leaf node, we should serialize it with // calc(..), except when one of the rgb color space functions are used, e.g. // rgb(..), hsl(..) or hwb(..) for historical reasons. // node.to_css(dest)?; }, ColorComponent::AlphaOmitted => { debug_assert!(false, "can't serialize an omitted alpha component"); }, } Ok(()) } } impl ToAnimatedValue for ColorComponent { type AnimatedValue = Self; fn to_animated_value(self, _context: &crate::values::animated::Context) -> Self::AnimatedValue { self } fn from_animated_value(animated: Self::AnimatedValue) -> Self { animated } } ================================================ FILE: style/color/convert.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Color conversion algorithms. //! //! Algorithms, matrices and constants are from the [color-4] specification, //! unless otherwise specified: //! //! https://drafts.csswg.org/css-color-4/#color-conversion-code //! //! NOTE: Matrices has to be transposed from the examples in the spec for use //! with the `euclid` library. use crate::color::ColorComponents; use crate::values::normalize; type Transform = euclid::default::Transform3D; type Vector = euclid::default::Vector3D; /// Normalize hue into [0, 360). #[inline] pub fn normalize_hue(hue: f32) -> f32 { hue - 360. * (hue / 360.).floor() } /// Calculate the hue from RGB components and return it along with the min and /// max RGB values. #[inline] fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { let max = red.max(green).max(blue); let min = red.min(green).min(blue); let delta = max - min; let hue = if delta != 0.0 { 60.0 * if max == red { (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } } else if max == green { (blue - red) / delta + 2.0 } else { (red - green) / delta + 4.0 } } else { f32::NAN }; (hue, min, max) } /// Convert from HSL notation to RGB notation. /// https://drafts.csswg.org/css-color-4/#hsl-to-rgb #[inline] pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { let hue = normalize_hue(hue); if hue * 6.0 < 360.0 { t1 + (t2 - t1) * hue / 60.0 } else if hue * 2.0 < 360.0 { t2 } else if hue * 3.0 < 720.0 { t1 + (t2 - t1) * (240.0 - hue) / 60.0 } else { t1 } } // Convert missing components to 0.0. let ColorComponents(hue, saturation, lightness) = from.map(normalize); let saturation = saturation / 100.0; let lightness = lightness / 100.0; let t2 = if lightness <= 0.5 { lightness * (saturation + 1.0) } else { lightness + saturation - lightness * saturation }; let t1 = lightness * 2.0 - t2; ColorComponents( hue_to_rgb(t1, t2, hue + 120.0), hue_to_rgb(t1, t2, hue), hue_to_rgb(t1, t2, hue - 120.0), ) } /// Convert from RGB notation to HSL notation. /// https://drafts.csswg.org/css-color-4/#rgb-to-hsl pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { let ColorComponents(red, green, blue) = *from; let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); let lightness = (min + max) / 2.0; let delta = max - min; let saturation = if delta != 0.0 { if lightness == 0.0 || lightness == 1.0 { 0.0 } else { (max - lightness) / lightness.min(1.0 - lightness) } } else { 0.0 }; ColorComponents(hue, saturation * 100.0, lightness * 100.0) } /// Convert from HWB notation to RGB notation. /// https://drafts.csswg.org/css-color-4/#hwb-to-rgb #[inline] pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { // Convert missing components to 0.0. let ColorComponents(hue, whiteness, blackness) = from.map(normalize); let whiteness = whiteness / 100.0; let blackness = blackness / 100.0; if whiteness + blackness >= 1.0 { let gray = whiteness / (whiteness + blackness); return ColorComponents(gray, gray, gray); } let x = 1.0 - whiteness - blackness; hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness) } /// Convert from RGB notation to HWB notation. /// https://drafts.csswg.org/css-color-4/#rgb-to-hwb #[inline] pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { let ColorComponents(red, green, blue) = *from; let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); let whiteness = min; let blackness = 1.0 - max; ColorComponents(hue, whiteness * 100.0, blackness * 100.0) } /// Calculate an epsilon for a specified range. #[inline] pub fn epsilon_for_range(min: f32, max: f32) -> f32 { (max - min) / 1.0e5 } /// Convert from the rectangular orthogonal to the cylindrical polar coordinate /// system. This is used to convert (ok)lab to (ok)lch. /// #[inline] pub fn orthogonal_to_polar(from: &ColorComponents, e: f32) -> ColorComponents { let ColorComponents(lightness, a, b) = *from; let chroma = (a * a + b * b).sqrt(); let hue = if a.abs() < e && b.abs() < e { // For extremely small values of a and b ... the reported hue angle // swinging about wildly and being essentially random ... this means // the hue is powerless, and treated as missing when converted into LCH // or Oklch. f32::NAN } else if chroma.abs() < e { // Very small chroma values make the hue component powerless. f32::NAN } else { normalize_hue(b.atan2(a).to_degrees()) }; ColorComponents(lightness, chroma, hue) } /// Convert from the cylindrical polar to the rectangular orthogonal coordinate /// system. This is used to convert (ok)lch to (ok)lab. /// #[inline] pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents { let ColorComponents(lightness, chroma, hue) = *from; // A missing hue component results in an achromatic color. if hue.is_nan() { return ColorComponents(lightness, 0.0, 0.0); } let hue = hue.to_radians(); let a = chroma * hue.cos(); let b = chroma * hue.sin(); ColorComponents(lightness, a, b) } #[inline] fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); ColorComponents(result.x, result.y, result.z) } fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { #[rustfmt::skip] const MAT: Transform = Transform::new( 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, 0.0, 0.0, 0.0, 1.0, ); transform(from, &MAT) } fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { #[rustfmt::skip] const MAT: Transform = Transform::new( 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, 0.0, 0.0, 0.0, 1.0, ); transform(from, &MAT) } /// A reference white that is used during color conversion. pub enum WhitePoint { /// D50 white reference. D50, /// D65 white reference. D65, } impl WhitePoint { const fn values(&self) -> ColorComponents { // match self { // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585] WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602), // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290] WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784), } } } fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { match (from, to) { (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), _ => {}, } } /// A trait that allows conversion of color spaces to and from XYZ coordinate /// space with a specified white point. /// /// Allows following the specified method of converting between color spaces: /// - Convert to values to sRGB linear light. /// - Convert to XYZ coordinate space. /// - Adjust white point to target white point. /// - Convert to sRGB linear light in target color space. /// - Convert to sRGB gamma encoded in target color space. /// /// https://drafts.csswg.org/css-color-4/#color-conversion pub trait ColorSpaceConversion { /// The white point that the implementer is represented in. const WHITE_POINT: WhitePoint; /// Convert the components from sRGB gamma encoded values to sRGB linear /// light values. fn to_linear_light(from: &ColorComponents) -> ColorComponents; /// Convert the components from sRGB linear light values to XYZ coordinate /// space. fn to_xyz(from: &ColorComponents) -> ColorComponents; /// Convert the components from XYZ coordinate space to sRGB linear light /// values. fn from_xyz(from: &ColorComponents) -> ColorComponents; /// Convert the components from sRGB linear light values to sRGB gamma /// encoded values. fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; } /// Convert the color components from the specified color space to XYZ and /// return the components and the white point they are in. pub fn to_xyz(from: &ColorComponents) -> (ColorComponents, WhitePoint) { // Convert the color components where in-gamut values are in the range // [0 - 1] to linear light (un-companded) form. let result = From::to_linear_light(from); // Convert the color components from the source color space to XYZ. (From::to_xyz(&result), From::WHITE_POINT) } /// Convert the color components from XYZ at the given white point to the /// specified color space. pub fn from_xyz( from: &ColorComponents, white_point: WhitePoint, ) -> ColorComponents { let mut xyz = from.clone(); // Convert the white point if needed. convert_white_point(white_point, To::WHITE_POINT, &mut xyz); // Convert the color from XYZ to the target color space. let result = To::from_xyz(&xyz); // Convert the color components of linear-light values in the range // [0 - 1] to a gamma corrected form. To::to_gamma_encoded(&result) } /// The sRGB color space. /// https://drafts.csswg.org/css-color-4/#predefined-sRGB pub struct Srgb; impl Srgb { #[rustfmt::skip] const TO_XYZ: Transform = Transform::new( 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const FROM_XYZ: Transform = Transform::new( 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for Srgb { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone().map(|value| { let abs = value.abs(); if abs < 0.04045 { value / 12.92 } else { value.signum() * ((abs + 0.055) / 1.055).powf(2.4) } }) } fn to_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::FROM_XYZ) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { from.clone().map(|value| { let abs = value.abs(); if abs > 0.0031308 { value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) } else { 12.92 * value } }) } } /// Color specified with hue, saturation and lightness components. pub struct Hsl; impl ColorSpaceConversion for Hsl { const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { Srgb::to_linear_light(&hsl_to_rgb(from)) } #[inline] fn to_xyz(from: &ColorComponents) -> ColorComponents { Srgb::to_xyz(from) } #[inline] fn from_xyz(from: &ColorComponents) -> ColorComponents { Srgb::from_xyz(from) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { rgb_to_hsl(&Srgb::to_gamma_encoded(from)) } } /// Color specified with hue, whiteness and blackness components. pub struct Hwb; impl ColorSpaceConversion for Hwb { const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { Srgb::to_linear_light(&hwb_to_rgb(from)) } #[inline] fn to_xyz(from: &ColorComponents) -> ColorComponents { Srgb::to_xyz(from) } #[inline] fn from_xyz(from: &ColorComponents) -> ColorComponents { Srgb::from_xyz(from) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { rgb_to_hwb(&Srgb::to_gamma_encoded(from)) } } /// The same as sRGB color space, except the transfer function is linear light. /// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear pub struct SrgbLinear; impl ColorSpaceConversion for SrgbLinear { const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { // Already in linear light form. from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { Srgb::to_xyz(from) } fn from_xyz(from: &ColorComponents) -> ColorComponents { Srgb::from_xyz(from) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { // Stay in linear light form. from.clone() } } /// The Display-P3 color space. /// https://drafts.csswg.org/css-color-4/#predefined-display-p3 pub struct DisplayP3; impl DisplayP3 { #[rustfmt::skip] const TO_XYZ: Transform = Transform::new( 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const FROM_XYZ: Transform = Transform::new( 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for DisplayP3 { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { Srgb::to_linear_light(from) } fn to_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::FROM_XYZ) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { Srgb::to_gamma_encoded(from) } } /// The Display-P3-linear color space. This is basically display-p3 without gamma encoding. pub struct DisplayP3Linear; impl ColorSpaceConversion for DisplayP3Linear { const WHITE_POINT: WhitePoint = DisplayP3::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { *from } fn to_xyz(from: &ColorComponents) -> ColorComponents { DisplayP3::to_xyz(from) } fn from_xyz(from: &ColorComponents) -> ColorComponents { DisplayP3::from_xyz(from) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { *from } } /// The a98-rgb color space. /// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb pub struct A98Rgb; impl A98Rgb { #[rustfmt::skip] const TO_XYZ: Transform = Transform::new( 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const FROM_XYZ: Transform = Transform::new( 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for A98Rgb { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) } fn to_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::FROM_XYZ) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { from.clone() .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) } } /// The ProPhoto RGB color space. /// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb pub struct ProphotoRgb; impl ProphotoRgb { #[rustfmt::skip] const TO_XYZ: Transform = Transform::new( 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const FROM_XYZ: Transform = Transform::new( 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for ProphotoRgb { const WHITE_POINT: WhitePoint = WhitePoint::D50; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone().map(|value| { const ET2: f32 = 16.0 / 512.0; let abs = value.abs(); if abs <= ET2 { value / 16.0 } else { value.signum() * abs.powf(1.8) } }) } fn to_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::FROM_XYZ) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { const ET: f32 = 1.0 / 512.0; from.clone().map(|v| { let abs = v.abs(); if abs >= ET { v.signum() * abs.powf(1.0 / 1.8) } else { 16.0 * v } }) } } /// The Rec.2020 color space. /// https://drafts.csswg.org/css-color-4/#predefined-rec2020 pub struct Rec2020; impl Rec2020 { const ALPHA: f32 = 1.09929682680944; const BETA: f32 = 0.018053968510807; #[rustfmt::skip] const TO_XYZ: Transform = Transform::new( 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const FROM_XYZ: Transform = Transform::new( 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for Rec2020 { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone().map(|value| { let abs = value.abs(); if abs < Self::BETA * 4.5 { value / 4.5 } else { value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) } }) } fn to_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { transform(from, &Self::FROM_XYZ) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { from.clone().map(|v| { let abs = v.abs(); if abs > Self::BETA { v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) } else { 4.5 * v } }) } } /// A color in the XYZ coordinate space with a D50 white reference. /// https://drafts.csswg.org/css-color-4/#predefined-xyz pub struct XyzD50; impl ColorSpaceConversion for XyzD50 { const WHITE_POINT: WhitePoint = WhitePoint::D50; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { from.clone() } fn from_xyz(from: &ColorComponents) -> ColorComponents { from.clone() } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { from.clone() } } /// A color in the XYZ coordinate space with a D65 white reference. /// https://drafts.csswg.org/css-color-4/#predefined-xyz pub struct XyzD65; impl ColorSpaceConversion for XyzD65 { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { from.clone() } fn from_xyz(from: &ColorComponents) -> ColorComponents { from.clone() } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { from.clone() } } /// The Lab color space. /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch pub struct Lab; impl Lab { const KAPPA: f32 = 24389.0 / 27.0; const EPSILON: f32 = 216.0 / 24389.0; } impl ColorSpaceConversion for Lab { const WHITE_POINT: WhitePoint = WhitePoint::D50; fn to_linear_light(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } /// Convert a CIELAB color to XYZ as specified in [1] and [2]. /// /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code fn to_xyz(from: &ColorComponents) -> ColorComponents { let ColorComponents(lightness, a, b) = *from; let f1 = (lightness + 16.0) / 116.0; let f0 = f1 + a / 500.0; let f2 = f1 - b / 200.0; let f0_cubed = f0 * f0 * f0; let x = if f0_cubed > Self::EPSILON { f0_cubed } else { (116.0 * f0 - 16.0) / Self::KAPPA }; let y = if lightness > Self::KAPPA * Self::EPSILON { let v = (lightness + 16.0) / 116.0; v * v * v } else { lightness / Self::KAPPA }; let f2_cubed = f2 * f2 * f2; let z = if f2_cubed > Self::EPSILON { f2_cubed } else { (116.0 * f2 - 16.0) / Self::KAPPA }; ColorComponents(x, y, z) * Self::WHITE_POINT.values() } /// Convert an XYZ color to LAB as specified in [1] and [2]. /// /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code fn from_xyz(from: &ColorComponents) -> ColorComponents { let adapted = *from / Self::WHITE_POINT.values(); // 4. Convert D50-adapted XYZ to Lab. let ColorComponents(f0, f1, f2) = adapted.map(|v| { if v > Self::EPSILON { v.cbrt() } else { (Self::KAPPA * v + 16.0) / 116.0 } }); let lightness = 116.0 * f1 - 16.0; let a = 500.0 * (f0 - f1); let b = 200.0 * (f1 - f2); ColorComponents(lightness, a, b) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } } /// The Lch color space. /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch pub struct Lch; impl ColorSpaceConversion for Lch { const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { // Convert LCH to Lab first. let lab = polar_to_orthogonal(from); // Then convert the Lab to XYZ. Lab::to_xyz(&lab) } fn from_xyz(from: &ColorComponents) -> ColorComponents { // First convert the XYZ to LAB. let lab = Lab::from_xyz(&from); // Then convert the Lab to LCH. orthogonal_to_polar(&lab, epsilon_for_range(0.0, 100.0)) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } } /// The Oklab color space. /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch pub struct Oklab; impl Oklab { #[rustfmt::skip] const XYZ_TO_LMS: Transform = Transform::new( 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const LMS_TO_OKLAB: Transform = Transform::new( 0.2104542553, 1.9779984951, 0.0259040371, 0.0, 0.7936177850, -2.4285922050, 0.7827717662, 0.0, -0.0040720468, 0.4505937099, -0.8086757660, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const LMS_TO_XYZ: Transform = Transform::new( 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, 0.0, 0.0, 0.0, 1.0, ); #[rustfmt::skip] const OKLAB_TO_LMS: Transform = Transform::new( 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, 0.0, 0.0, 0.0, 1.0, ); } impl ColorSpaceConversion for Oklab { const WHITE_POINT: WhitePoint = WhitePoint::D65; fn to_linear_light(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { let lms = transform(&from, &Self::OKLAB_TO_LMS); let lms = lms.map(|v| v * v * v); transform(&lms, &Self::LMS_TO_XYZ) } fn from_xyz(from: &ColorComponents) -> ColorComponents { let lms = transform(&from, &Self::XYZ_TO_LMS); let lms = lms.map(|v| v.cbrt()); transform(&lms, &Self::LMS_TO_OKLAB) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } } /// The Oklch color space. /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch pub struct Oklch; impl ColorSpaceConversion for Oklch { const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; fn to_linear_light(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } fn to_xyz(from: &ColorComponents) -> ColorComponents { // First convert OkLCH to Oklab. let oklab = polar_to_orthogonal(from); // Then convert Oklab to XYZ. Oklab::to_xyz(&oklab) } fn from_xyz(from: &ColorComponents) -> ColorComponents { // First convert XYZ to Oklab. let lab = Oklab::from_xyz(&from); // Then convert Oklab to OkLCH. orthogonal_to_polar(&lab, epsilon_for_range(0.0, 1.0)) } fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { // No need for conversion. from.clone() } } ================================================ FILE: style/color/mix.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Color mixing/interpolation. use super::{AbsoluteColor, ColorFlags, ColorSpace}; use crate::color::ColorMixItemList; use crate::derives::*; use crate::parser::{Parse, ParserContext}; use crate::values::generics::color::ColorMixFlags; use cssparser::Parser; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, ToCss}; /// A hue-interpolation-method as defined in [1]. /// /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToAnimatedValue, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum HueInterpolationMethod { /// https://drafts.csswg.org/css-color-4/#shorter Shorter, /// https://drafts.csswg.org/css-color-4/#longer Longer, /// https://drafts.csswg.org/css-color-4/#increasing Increasing, /// https://drafts.csswg.org/css-color-4/#decreasing Decreasing, /// https://drafts.csswg.org/css-color-4/#specified Specified, } /// https://drafts.csswg.org/css-color-4/#color-interpolation-method #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem, ToAnimatedValue, ToComputedValue, ToResolvedValue, )] #[repr(C)] pub struct ColorInterpolationMethod { /// The color-space the interpolation should be done in. pub space: ColorSpace, /// The hue interpolation method. pub hue: HueInterpolationMethod, } impl ColorInterpolationMethod { /// Returns the srgb interpolation method. pub const fn srgb() -> Self { Self { space: ColorSpace::Srgb, hue: HueInterpolationMethod::Shorter, } } /// Return the oklab interpolation method used for default color /// interpolcation. pub const fn oklab() -> Self { Self { space: ColorSpace::Oklab, hue: HueInterpolationMethod::Shorter, } } /// Return true if the this is the default method. pub fn is_default(&self) -> bool { self.space == ColorSpace::Oklab } /// Decides the best method for interpolating between the given colors. /// https://drafts.csswg.org/css-color-4/#interpolation-space pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { // The default color space to use for interpolation is Oklab. However, // if either of the colors are in legacy rgb(), hsl() or hwb(), then // interpolation is done in sRGB. if !left.is_legacy_syntax() || !right.is_legacy_syntax() { Self::default() } else { Self::srgb() } } } impl Default for ColorInterpolationMethod { fn default() -> Self { Self::oklab() } } impl Parse for ColorInterpolationMethod { fn parse<'i, 't>( _: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_ident_matching("in")?; let space = ColorSpace::parse(input)?; // https://drafts.csswg.org/css-color-4/#hue-interpolation // Unless otherwise specified, if no specific hue interpolation // algorithm is selected by the host syntax, the default is shorter. let hue = if space.is_polar() { input .try_parse(|input| -> Result<_, ParseError<'i>> { let hue = HueInterpolationMethod::parse(input)?; input.expect_ident_matching("hue")?; Ok(hue) }) .unwrap_or(HueInterpolationMethod::Shorter) } else { HueInterpolationMethod::Shorter }; Ok(Self { space, hue }) } } impl ToCss for ColorInterpolationMethod { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { dest.write_str("in ")?; self.space.to_css(dest)?; if self.hue != HueInterpolationMethod::Shorter { dest.write_char(' ')?; self.hue.to_css(dest)?; dest.write_str(" hue")?; } Ok(()) } } /// A color and its weight for use in a color mix. pub struct ColorMixItem { /// The color being mixed. pub color: AbsoluteColor, /// How much this color contributes to the final mix. pub weight: f32, } impl ColorMixItem { /// Create a new color item for mixing. #[inline] pub fn new(color: AbsoluteColor, weight: f32) -> Self { Self { color, weight } } } /// Mix N colors into one (left-to-right fold). pub fn mix_many( interpolation: ColorInterpolationMethod, items: impl IntoIterator, flags: ColorMixFlags, ) -> AbsoluteColor { let items = items.into_iter().collect::>(); // Match the behavior when the sum of weights equal 0. if items.is_empty() { return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space); } let normalize = flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS); let mut weight_scale = 1.0; let mut alpha_multiplier = 1.0; if normalize { // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm let sum: f32 = items.iter().map(|item| item.weight).sum(); if sum == 0.0 { return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space); } if (sum - 1.0).abs() > f32::EPSILON { weight_scale = 1.0 / sum; if sum < 1.0 { alpha_multiplier = sum; } } } // We can unwrap here, because we already checked for no items. let (first, rest) = items.split_first().unwrap(); let mut accumulated_color = convert_for_mix(&first.color, interpolation.space); let mut accumulated_weight = first.weight * weight_scale; for item in rest { let weight = item.weight * weight_scale; let combined = accumulated_weight + weight; if combined == 0.0 { // If both are 0, this fold doesn't contribute anything to the result. continue; } let right = convert_for_mix(&item.color, interpolation.space); let (left_weight, right_weight) = if normalize { (accumulated_weight / combined, weight / combined) } else { (accumulated_weight, weight) }; accumulated_color = mix_with_weights( &accumulated_color, left_weight, &right, right_weight, interpolation.hue, ); accumulated_weight = combined; } let components = accumulated_color.raw_components(); let alpha = components[3] * alpha_multiplier; // FIXME: In rare cases we end up with 0.999995 in the alpha channel, // so we reduce the precision to avoid serializing to // rgba(?, ?, ?, 1). This is not ideal, so we should look into // ways to avoid it. Maybe pre-multiply all color components and // then divide after calculations? let alpha = (alpha.clamp(0.0, 1.0) * 1000.0).round() / 1000.0; let mut result = AbsoluteColor::new( interpolation.space, components[0], components[1], components[2], alpha, ); result.flags = accumulated_color.flags; if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { // If the result *MUST* be in modern syntax, then make sure it is in a // color space that allows the modern syntax. So hsl and hwb will be // converted to srgb. if result.is_legacy_syntax() { result.to_color_space(ColorSpace::Srgb) } else { result } } else if items.iter().all(|item| item.color.is_legacy_syntax()) { // If both sides of the mix is legacy then convert the result back into // legacy. result.into_srgb_legacy() } else { result } } /// What the outcome of each component should be in a mix result. #[derive(Clone, Copy, PartialEq)] #[repr(u8)] enum ComponentMixOutcome { /// Mix the left and right sides to give the result. Mix, /// Carry the left side forward to the result. UseLeft, /// Carry the right side forward to the result. UseRight, /// The resulting component should also be none. None, } impl ComponentMixOutcome { fn from_colors( left: &AbsoluteColor, right: &AbsoluteColor, flags_to_check: ColorFlags, ) -> Self { match ( left.flags.contains(flags_to_check), right.flags.contains(flags_to_check), ) { (true, true) => Self::None, (true, false) => Self::UseRight, (false, true) => Self::UseLeft, (false, false) => Self::Mix, } } } impl AbsoluteColor { /// Calculate the flags that should be carried forward a color before converting /// it to the interpolation color space according to: /// fn carry_forward_analogous_missing_components(&mut self, source: &AbsoluteColor) { use ColorFlags as F; use ColorSpace as S; if source.color_space == self.color_space { return; } // Reds r, x // Greens g, y // Blues b, z if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() { return; } // Lightness L if matches!(source.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) { if matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) { self.flags |= source.flags & F::C0_IS_NONE; } else if matches!(self.color_space, S::Hsl) { if source.flags.contains(F::C0_IS_NONE) { self.flags.insert(F::C2_IS_NONE) } } } else if matches!(source.color_space, S::Hsl) && matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) { if source.flags.contains(F::C2_IS_NONE) { self.flags.insert(F::C0_IS_NONE) } } // Colorfulness C, S if matches!(source.color_space, S::Hsl | S::Lch | S::Oklch) && matches!(self.color_space, S::Hsl | S::Lch | S::Oklch) { self.flags |= source.flags & F::C1_IS_NONE; } // Hue H if matches!(source.color_space, S::Hsl | S::Hwb) { if matches!(self.color_space, S::Hsl | S::Hwb) { self.flags |= source.flags & F::C0_IS_NONE; } else if matches!(self.color_space, S::Lch | S::Oklch) { if source.flags.contains(F::C0_IS_NONE) { self.flags.insert(F::C2_IS_NONE) } } } else if matches!(source.color_space, S::Lch | S::Oklch) { if matches!(self.color_space, S::Hsl | S::Hwb) { if source.flags.contains(F::C2_IS_NONE) { self.flags.insert(F::C0_IS_NONE) } } else if matches!(self.color_space, S::Lch | S::Oklch) { self.flags |= source.flags & F::C2_IS_NONE; } } // Opponent a, a // Opponent b, b if matches!(source.color_space, S::Lab | S::Oklab) && matches!(self.color_space, S::Lab | S::Oklab) { self.flags |= source.flags & F::C1_IS_NONE; self.flags |= source.flags & F::C2_IS_NONE; } } } /// Mix two colors already in the interpolation color space. fn mix_with_weights( left: &AbsoluteColor, left_weight: f32, right: &AbsoluteColor, right_weight: f32, hue_interpolation: HueInterpolationMethod, ) -> AbsoluteColor { debug_assert!(right.color_space == left.color_space); let color_space = left.color_space; let outcomes = [ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE), ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE), ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE), ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE), ]; // Convert both sides into just components. let left = left.raw_components(); let right = right.raw_components(); let (result, result_flags) = interpolate_premultiplied( &left, left_weight, &right, right_weight, color_space.hue_index(), hue_interpolation, &outcomes, ); let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], result[3]); result.flags = result_flags; result } fn convert_for_mix(color: &AbsoluteColor, color_space: ColorSpace) -> AbsoluteColor { let mut converted = color.to_color_space(color_space); converted.carry_forward_analogous_missing_components(color); converted } fn interpolate_premultiplied_component( left: f32, left_weight: f32, left_alpha: f32, right: f32, right_weight: f32, right_alpha: f32, ) -> f32 { left * left_weight * left_alpha + right * right_weight * right_alpha } // Normalize hue into [0, 360) #[inline] fn normalize_hue(v: f32) -> f32 { v - 360. * (v / 360.).floor() } fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { // Adjust the hue angle as per // https://drafts.csswg.org/css-color/#hue-interpolation. // // If both hue angles are NAN, they should be set to 0. Otherwise, if a // single hue angle is NAN, it should use the other hue angle. if left.is_nan() { if right.is_nan() { *left = 0.; *right = 0.; } else { *left = *right; } } else if right.is_nan() { *right = *left; } if hue_interpolation == HueInterpolationMethod::Specified { // Angles are not adjusted. They are interpolated like any other // component. return; } *left = normalize_hue(*left); *right = normalize_hue(*right); match hue_interpolation { // https://drafts.csswg.org/css-color/#shorter HueInterpolationMethod::Shorter => { let delta = *right - *left; if delta > 180. { *left += 360.; } else if delta < -180. { *right += 360.; } }, // https://drafts.csswg.org/css-color/#longer HueInterpolationMethod::Longer => { let delta = *right - *left; if 0. < delta && delta < 180. { *left += 360.; } else if -180. < delta && delta <= 0. { *right += 360.; } }, // https://drafts.csswg.org/css-color/#increasing HueInterpolationMethod::Increasing => { if *right < *left { *right += 360.; } }, // https://drafts.csswg.org/css-color/#decreasing HueInterpolationMethod::Decreasing => { if *left < *right { *left += 360.; } }, HueInterpolationMethod::Specified => unreachable!("Handled above"), } } fn interpolate_hue( mut left: f32, left_weight: f32, mut right: f32, right_weight: f32, hue_interpolation: HueInterpolationMethod, ) -> f32 { adjust_hue(&mut left, &mut right, hue_interpolation); left * left_weight + right * right_weight } struct InterpolatedAlpha { /// The adjusted left alpha value. left: f32, /// The adjusted right alpha value. right: f32, /// The interpolated alpha value. interpolated: f32, /// Whether the alpha component should be `none`. is_none: bool, } fn interpolate_alpha( left: f32, left_weight: f32, right: f32, right_weight: f32, outcome: ComponentMixOutcome, ) -> InterpolatedAlpha { // let mut result = match outcome { ComponentMixOutcome::Mix => { let interpolated = left * left_weight + right * right_weight; InterpolatedAlpha { left, right, interpolated, is_none: false, } }, ComponentMixOutcome::UseLeft => InterpolatedAlpha { left, right: left, interpolated: left, is_none: false, }, ComponentMixOutcome::UseRight => InterpolatedAlpha { left: right, right, interpolated: right, is_none: false, }, ComponentMixOutcome::None => InterpolatedAlpha { left: 1.0, right: 1.0, interpolated: 0.0, is_none: true, }, }; // Clip all alpha values to [0.0..1.0]. result.left = result.left.clamp(0.0, 1.0); result.right = result.right.clamp(0.0, 1.0); result.interpolated = result.interpolated.clamp(0.0, 1.0); result } fn interpolate_premultiplied( left: &[f32; 4], left_weight: f32, right: &[f32; 4], right_weight: f32, hue_index: Option, hue_interpolation: HueInterpolationMethod, outcomes: &[ComponentMixOutcome; 4], ) -> ([f32; 4], ColorFlags) { let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]); let mut flags = if alpha.is_none { ColorFlags::ALPHA_IS_NONE } else { ColorFlags::empty() }; let mut result = [0.; 4]; for i in 0..3 { match outcomes[i] { ComponentMixOutcome::Mix => { let is_hue = hue_index == Some(i); result[i] = if is_hue { normalize_hue(interpolate_hue( left[i], left_weight, right[i], right_weight, hue_interpolation, )) } else { let interpolated = interpolate_premultiplied_component( left[i], left_weight, alpha.left, right[i], right_weight, alpha.right, ); if alpha.interpolated == 0.0 { interpolated } else { interpolated / alpha.interpolated } }; }, ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => { let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft { left[i] } else { right[i] }; result[i] = if hue_interpolation == HueInterpolationMethod::Longer && hue_index == Some(i) { // If "longer hue" interpolation is required, we have to actually do // the computation even if we're using the same value at both ends, // so that interpolating from the starting hue back to the same value // produces a full cycle, rather than a constant hue. normalize_hue(interpolate_hue( used_component, left_weight, used_component, right_weight, hue_interpolation, )) } else { used_component }; }, ComponentMixOutcome::None => { result[i] = 0.0; match i { 0 => flags.insert(ColorFlags::C0_IS_NONE), 1 => flags.insert(ColorFlags::C1_IS_NONE), 2 => flags.insert(ColorFlags::C2_IS_NONE), _ => unreachable!(), } }, } } result[3] = alpha.interpolated; (result, flags) } ================================================ FILE: style/color/mod.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Color support functions. /// cbindgen:ignore pub mod convert; mod color_function; pub mod component; pub mod mix; pub mod parsing; mod to_css; use self::parsing::ChannelKeyword; use crate::derives::*; pub use color_function::*; use component::ColorComponent; use cssparser::color::PredefinedColorSpace; /// Number of color-mix items to reserve on the stack to avoid heap allocations. pub const PRE_ALLOCATED_COLOR_MIX_ITEMS: usize = 3; /// Conveniece type to use for collecting color mix items. pub type ColorMixItemList = smallvec::SmallVec<[T; PRE_ALLOCATED_COLOR_MIX_ITEMS]>; /// The 3 components that make up a color. (Does not include the alpha component) #[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(C)] pub struct ColorComponents(pub f32, pub f32, pub f32); impl ColorComponents { /// Apply a function to each of the 3 components of the color. #[must_use] pub fn map(self, f: impl Fn(f32) -> f32) -> Self { Self(f(self.0), f(self.1), f(self.2)) } } impl std::ops::Mul for ColorComponents { type Output = Self; fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2) } } impl std::ops::Div for ColorComponents { type Output = Self; fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2) } } /// A color space representation in the CSS specification. /// /// https://drafts.csswg.org/css-color-4/#typedef-color-space #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToAnimatedValue, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(u8)] pub enum ColorSpace { /// A color specified in the sRGB color space with either the rgb/rgba(..) /// functions or the newer color(srgb ..) function. If the color(..) /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples: /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)" Srgb = 0, /// A color specified in the Hsl notation in the sRGB color space, e.g. /// "hsl(289.18 93.136% 65.531%)" /// https://drafts.csswg.org/css-color-4/#the-hsl-notation Hsl, /// A color specified in the Hwb notation in the sRGB color space, e.g. /// "hwb(740deg 20% 30%)" /// https://drafts.csswg.org/css-color-4/#the-hwb-notation Hwb, /// A color specified in the Lab color format, e.g. /// "lab(29.2345% 39.3825 20.0664)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors Lab, /// A color specified in the Lch color format, e.g. /// "lch(29.2345% 44.2 27)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors Lch, /// A color specified in the Oklab color format, e.g. /// "oklab(40.101% 0.1147 0.0453)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors Oklab, /// A color specified in the Oklch color format, e.g. /// "oklch(40.101% 0.12332 21.555)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors Oklch, /// A color specified with the color(..) function and the "srgb-linear" /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)". SrgbLinear, /// A color specified with the color(..) function and the "display-p3" /// color space, e.g. "color(display-p3 0.84 0.19 0.72)". DisplayP3, /// A color specified with the color(..) function and the "display-p3-linear" /// color space. DisplayP3Linear, /// A color specified with the color(..) function and the "a98-rgb" color /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)". A98Rgb, /// A color specified with the color(..) function and the "prophoto-rgb" /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)". ProphotoRgb, /// A color specified with the color(..) function and the "rec2020" color /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)". Rec2020, /// A color specified with the color(..) function and the "xyz-d50" color /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)". XyzD50, /// A color specified with the color(..) function and the "xyz-d65" or "xyz" /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values /// specifies that `xyz` is an alias for the `xyz-d65` color space. #[parse(aliases = "xyz")] XyzD65, } impl ColorSpace { /// Returns whether this is a ``. #[inline] pub fn is_rectangular(&self) -> bool { !self.is_polar() } /// Returns whether this is a ``. #[inline] pub fn is_polar(&self) -> bool { matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) } /// Returns true if the color has RGB or XYZ components. #[inline] pub fn is_rgb_or_xyz_like(&self) -> bool { match self { Self::Srgb | Self::SrgbLinear | Self::DisplayP3 | Self::DisplayP3Linear | Self::A98Rgb | Self::ProphotoRgb | Self::Rec2020 | Self::XyzD50 | Self::XyzD65 => true, _ => false, } } /// Returns an index of the hue component in the color space, otherwise /// `None`. #[inline] pub fn hue_index(&self) -> Option { match self { Self::Hsl | Self::Hwb => Some(0), Self::Lch | Self::Oklch => Some(2), _ => { debug_assert!(!self.is_polar()); None }, } } } /// Flags used when serializing colors. #[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[repr(C)] pub struct ColorFlags(u8); bitflags! { impl ColorFlags : u8 { /// Whether the 1st color component is `none`. const C0_IS_NONE = 1 << 0; /// Whether the 2nd color component is `none`. const C1_IS_NONE = 1 << 1; /// Whether the 3rd color component is `none`. const C2_IS_NONE = 1 << 2; /// Whether the alpha component is `none`. const ALPHA_IS_NONE = 1 << 3; /// Marks that this color is in the legacy color format. This flag is /// only valid for the `Srgb` color space. const IS_LEGACY_SRGB = 1 << 4; } } /// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), /// oklab(), oklch() or color(). #[derive(Copy, Clone, Debug, MallocSizeOf, ToShmem, ToTyped)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(C)] #[typed(todo_derive_fields)] pub struct AbsoluteColor { /// The 3 components that make up colors in any color space. pub components: ColorComponents, /// The alpha component of the color. pub alpha: f32, /// The current color space that the components represent. pub color_space: ColorSpace, /// Extra flags used during serialization of this color. pub flags: ColorFlags, } impl PartialEq for AbsoluteColor { // See https://github.com/w3c/csswg-drafts/issues/13157#issuecomment-4165667681 fn eq(&self, other: &Self) -> bool { let none_flags = ColorFlags::C0_IS_NONE | ColorFlags::C1_IS_NONE | ColorFlags::C2_IS_NONE | ColorFlags::ALPHA_IS_NONE; // If both colors have the same color-space, just compare components; note that // any `none` components only match `none` in the other color. if self.color_space == other.color_space { return self.components == other.components && self.alpha == other.alpha && (self.flags & none_flags) == (other.flags & none_flags); } // Otherwise, if any `none` components are present in either color, return false. if self.flags.union(other.flags).intersects(none_flags) { return false; } // Otherwise, convert both colors to Oklab for comparison, and allow EPSILON // difference in component values. // TODO: check value of EPSILON once the spec is updated to cover this. const EPSILON: f32 = 0.0001; let a = self.to_color_space(ColorSpace::Oklab); let b = other.to_color_space(ColorSpace::Oklab); (a.components.0 - b.components.0).abs() <= EPSILON && (a.components.1 - b.components.1).abs() <= EPSILON && (a.components.2 - b.components.2).abs() <= EPSILON && (a.alpha - b.alpha).abs() <= EPSILON } } /// Given an [`AbsoluteColor`], return the 4 float components as the type given, /// e.g.: /// /// ```rust /// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); /// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] /// ``` macro_rules! color_components_as { ($c:expr, $t:ty) => {{ // This macro is not an inline function, because we can't use the // generic type ($t) in a constant expression as per: // https://github.com/rust-lang/rust/issues/76560 const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>()); const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>()); const_assert!(std::mem::size_of::() >= std::mem::size_of::<$t>()); const_assert_eq!( std::mem::align_of::(), std::mem::align_of::<$t>() ); std::mem::transmute::<&ColorComponents, &$t>(&$c.components) }}; } /// Holds details about each component passed into creating a new [`AbsoluteColor`]. pub struct ComponentDetails { value: f32, is_none: bool, } impl From for ComponentDetails { fn from(value: f32) -> Self { Self { value, is_none: false, } } } impl From for ComponentDetails { fn from(value: u8) -> Self { Self { value: value as f32 / 255.0, is_none: false, } } } impl From> for ComponentDetails { fn from(value: Option) -> Self { if let Some(value) = value { Self { value, is_none: false, } } else { Self { value: 0.0, is_none: true, } } } } impl From> for ComponentDetails { fn from(value: ColorComponent) -> Self { if let ColorComponent::Value(value) = value { Self { value, is_none: false, } } else { Self { value: 0.0, is_none: true, } } } } impl AbsoluteColor { /// A fully transparent color in the legacy syntax. pub const TRANSPARENT_BLACK: Self = Self { components: ColorComponents(0.0, 0.0, 0.0), alpha: 0.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// An opaque black color in the legacy syntax. pub const BLACK: Self = Self { components: ColorComponents(0.0, 0.0, 0.0), alpha: 1.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// An opaque white color in the legacy syntax. pub const WHITE: Self = Self { components: ColorComponents(1.0, 1.0, 1.0), alpha: 1.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and /// components. pub fn new( color_space: ColorSpace, c1: impl Into, c2: impl Into, c3: impl Into, alpha: impl Into, ) -> Self { let mut flags = ColorFlags::empty(); macro_rules! cd { ($c:expr,$flag:expr) => {{ let component_details = $c.into(); if component_details.is_none { flags |= $flag; } component_details.value }}; } let mut components = ColorComponents( cd!(c1, ColorFlags::C0_IS_NONE), cd!(c2, ColorFlags::C1_IS_NONE), cd!(c3, ColorFlags::C2_IS_NONE), ); let alpha = cd!(alpha, ColorFlags::ALPHA_IS_NONE); // Lightness for Lab and Lch is clamped to [0..100]. if matches!(color_space, ColorSpace::Lab | ColorSpace::Lch) { components.0 = components.0.clamp(0.0, 100.0); } // Lightness for Oklab and Oklch is clamped to [0..1]. if matches!(color_space, ColorSpace::Oklab | ColorSpace::Oklch) { components.0 = components.0.clamp(0.0, 1.0); } // Chroma must not be less than 0. if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) { components.1 = components.1.max(0.0); } // Alpha is always clamped to [0..1]. let alpha = alpha.clamp(0.0, 1.0); Self { components, alpha, color_space, flags, } } /// Convert this color into the sRGB color space and set it to the legacy /// syntax. #[inline] #[must_use] pub fn into_srgb_legacy(self) -> Self { let mut result = if !matches!(self.color_space, ColorSpace::Srgb) { self.to_color_space(ColorSpace::Srgb) } else { self }; // Explicitly set the flags to IS_LEGACY_SRGB only to clear out the // *_IS_NONE flags, because the legacy syntax doesn't allow "none". result.flags = ColorFlags::IS_LEGACY_SRGB; result } /// Create a new [`AbsoluteColor`] from rgba legacy syntax values in the sRGB color space. pub fn srgb_legacy(red: u8, green: u8, blue: u8, alpha: f32) -> Self { let mut result = Self::new(ColorSpace::Srgb, red, green, blue, alpha); result.flags = ColorFlags::IS_LEGACY_SRGB; result } /// Return all the components of the color in an array. (Includes alpha) #[inline] pub fn raw_components(&self) -> &[f32; 4] { unsafe { color_components_as!(self, [f32; 4]) } } /// Returns true if this color is in the legacy color syntax. #[inline] pub fn is_legacy_syntax(&self) -> bool { // rgb(), rgba(), hsl(), hsla(), hwb(), hwba() match self.color_space { ColorSpace::Srgb => self.flags.contains(ColorFlags::IS_LEGACY_SRGB), ColorSpace::Hsl | ColorSpace::Hwb => true, _ => false, } } /// Returns true if this color is fully transparent. #[inline] pub fn is_transparent(&self) -> bool { self.flags.contains(ColorFlags::ALPHA_IS_NONE) || self.alpha == 0.0 } /// Return an optional first component. #[inline] pub fn c0(&self) -> Option { if self.flags.contains(ColorFlags::C0_IS_NONE) { None } else { Some(self.components.0) } } /// Return an optional second component. #[inline] pub fn c1(&self) -> Option { if self.flags.contains(ColorFlags::C1_IS_NONE) { None } else { Some(self.components.1) } } /// Return an optional second component. #[inline] pub fn c2(&self) -> Option { if self.flags.contains(ColorFlags::C2_IS_NONE) { None } else { Some(self.components.2) } } /// Return an optional alpha component. #[inline] pub fn alpha(&self) -> Option { if self.flags.contains(ColorFlags::ALPHA_IS_NONE) { None } else { Some(self.alpha) } } /// Return the value of a component by its channel keyword. pub fn get_component_by_channel_keyword( &self, channel_keyword: ChannelKeyword, ) -> Result, ()> { if channel_keyword == ChannelKeyword::Alpha { return Ok(self.alpha()); } Ok(match self.color_space { ColorSpace::Srgb => { if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) { match channel_keyword { ChannelKeyword::R => self.c0().map(|v| v * 255.0), ChannelKeyword::G => self.c1().map(|v| v * 255.0), ChannelKeyword::B => self.c2().map(|v| v * 255.0), _ => return Err(()), } } else { match channel_keyword { ChannelKeyword::R => self.c0(), ChannelKeyword::G => self.c1(), ChannelKeyword::B => self.c2(), _ => return Err(()), } } }, ColorSpace::Hsl => match channel_keyword { ChannelKeyword::H => self.c0(), ChannelKeyword::S => self.c1(), ChannelKeyword::L => self.c2(), _ => return Err(()), }, ColorSpace::Hwb => match channel_keyword { ChannelKeyword::H => self.c0(), ChannelKeyword::W => self.c1(), ChannelKeyword::B => self.c2(), _ => return Err(()), }, ColorSpace::Lab | ColorSpace::Oklab => match channel_keyword { ChannelKeyword::L => self.c0(), ChannelKeyword::A => self.c1(), ChannelKeyword::B => self.c2(), _ => return Err(()), }, ColorSpace::Lch | ColorSpace::Oklch => match channel_keyword { ChannelKeyword::L => self.c0(), ChannelKeyword::C => self.c1(), ChannelKeyword::H => self.c2(), _ => return Err(()), }, ColorSpace::SrgbLinear | ColorSpace::DisplayP3 | ColorSpace::DisplayP3Linear | ColorSpace::A98Rgb | ColorSpace::ProphotoRgb | ColorSpace::Rec2020 => match channel_keyword { ChannelKeyword::R => self.c0(), ChannelKeyword::G => self.c1(), ChannelKeyword::B => self.c2(), _ => return Err(()), }, ColorSpace::XyzD50 | ColorSpace::XyzD65 => match channel_keyword { ChannelKeyword::X => self.c0(), ChannelKeyword::Y => self.c1(), ChannelKeyword::Z => self.c2(), _ => return Err(()), }, }) } /// Convert this color to the specified color space. pub fn to_color_space(&self, color_space: ColorSpace) -> Self { use ColorSpace::*; if self.color_space == color_space { return self.clone(); } // Conversion functions doesn't handle NAN component values, so they are // converted to 0.0. They do however need to know if a component is // missing, so we use NAN as the marker for that. macro_rules! missing_to_nan { ($c:expr) => {{ if let Some(v) = $c { crate::values::normalize(v) } else { f32::NAN } }}; } let components = ColorComponents( missing_to_nan!(self.c0()), missing_to_nan!(self.c1()), missing_to_nan!(self.c2()), ); let result = match (self.color_space, color_space) { // We have simplified conversions that do not need to convert to XYZ // first. This improves performance, because it skips at least 2 // matrix multiplications and reduces float rounding errors. (Srgb, Hsl) => convert::rgb_to_hsl(&components), (Srgb, Hwb) => convert::rgb_to_hwb(&components), (Hsl, Srgb) => convert::hsl_to_rgb(&components), (Hwb, Srgb) => convert::hwb_to_rgb(&components), (Lab, Lch) | (Oklab, Oklch) => convert::orthogonal_to_polar( &components, convert::epsilon_for_range(0.0, if color_space == Lch { 100.0 } else { 1.0 }), ), (Lch, Lab) | (Oklch, Oklab) => convert::polar_to_orthogonal(&components), // All other conversions need to convert to XYZ first. _ => { let (xyz, white_point) = match self.color_space { Lab => convert::to_xyz::(&components), Lch => convert::to_xyz::(&components), Oklab => convert::to_xyz::(&components), Oklch => convert::to_xyz::(&components), Srgb => convert::to_xyz::(&components), Hsl => convert::to_xyz::(&components), Hwb => convert::to_xyz::(&components), SrgbLinear => convert::to_xyz::(&components), DisplayP3 => convert::to_xyz::(&components), DisplayP3Linear => convert::to_xyz::(&components), A98Rgb => convert::to_xyz::(&components), ProphotoRgb => convert::to_xyz::(&components), Rec2020 => convert::to_xyz::(&components), XyzD50 => convert::to_xyz::(&components), XyzD65 => convert::to_xyz::(&components), }; match color_space { Lab => convert::from_xyz::(&xyz, white_point), Lch => convert::from_xyz::(&xyz, white_point), Oklab => convert::from_xyz::(&xyz, white_point), Oklch => convert::from_xyz::(&xyz, white_point), Srgb => convert::from_xyz::(&xyz, white_point), Hsl => convert::from_xyz::(&xyz, white_point), Hwb => convert::from_xyz::(&xyz, white_point), SrgbLinear => convert::from_xyz::(&xyz, white_point), DisplayP3 => convert::from_xyz::(&xyz, white_point), DisplayP3Linear => { convert::from_xyz::(&xyz, white_point) }, A98Rgb => convert::from_xyz::(&xyz, white_point), ProphotoRgb => convert::from_xyz::(&xyz, white_point), Rec2020 => convert::from_xyz::(&xyz, white_point), XyzD50 => convert::from_xyz::(&xyz, white_point), XyzD65 => convert::from_xyz::(&xyz, white_point), } }, }; // A NAN value coming from a conversion function means the the component // is missing, so we convert it to None. macro_rules! nan_to_missing { ($v:expr) => {{ if $v.is_nan() { None } else { Some($v) } }}; } Self::new( color_space, nan_to_missing!(result.0), nan_to_missing!(result.1), nan_to_missing!(result.2), self.alpha(), ) } /// Convert a color value to `nscolor`. pub fn to_nscolor(&self) -> u32 { let srgb = self.to_color_space(ColorSpace::Srgb); u32::from_le_bytes([ (srgb.components.0 * 255.0).round() as u8, (srgb.components.1 * 255.0).round() as u8, (srgb.components.2 * 255.0).round() as u8, (srgb.alpha * 255.0).round() as u8, ]) } /// Convert a given `nscolor` to a Servo AbsoluteColor value. pub fn from_nscolor(color: u32) -> Self { let [r, g, b, a] = color.to_le_bytes(); Self::srgb_legacy(r, g, b, a as f32 / 255.0) } } #[test] fn from_nscolor_should_be_in_legacy_syntax() { let result = AbsoluteColor::from_nscolor(0x336699CC); assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB)); assert!(result.is_legacy_syntax()); } impl From for ColorSpace { fn from(value: PredefinedColorSpace) -> Self { match value { PredefinedColorSpace::Srgb => ColorSpace::Srgb, PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, PredefinedColorSpace::DisplayP3Linear => ColorSpace::DisplayP3Linear, PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, } } } ================================================ FILE: style/color/parsing.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #![deny(missing_docs)] //! Parsing for CSS colors. use super::{ color_function::ColorFunction, component::{ColorComponent, ColorComponentType}, AbsoluteColor, }; use crate::derives::*; use crate::{ parser::{Parse, ParserContext}, values::{ generics::{calc::CalcUnits, Optional}, specified::{angle::Angle as SpecifiedAngle, calc::Leaf, color::Color as SpecifiedColor}, }, }; use cssparser::{ color::{parse_hash_color, PredefinedColorSpace, OPAQUE}, match_ignore_ascii_case, CowRcStr, Parser, Token, }; use style_traits::{ParseError, StyleParseErrorKind}; /// Returns true if the relative color syntax pref is enabled. #[inline] pub fn rcs_enabled() -> bool { static_prefs::pref!("layout.css.relative-color-syntax.enabled") } /// Represents a channel keyword inside a color. #[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, PartialOrd, ToCss, ToShmem)] #[repr(u8)] pub enum ChannelKeyword { /// alpha Alpha, /// a A, /// b, blackness, blue B, /// chroma C, /// green G, /// hue H, /// lightness L, /// red R, /// saturation S, /// whiteness W, /// x X, /// y Y, /// z Z, } /// Return the named color with the given name. /// /// Matching is case-insensitive in the ASCII range. /// CSS escaping (if relevant) should be resolved before calling this function. /// (For example, the value of an `Ident` token is fine.) #[inline] pub fn parse_color_keyword(ident: &str) -> Result { Ok(match_ignore_ascii_case! { ident, "transparent" => { SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0)) }, "currentcolor" => SpecifiedColor::CurrentColor, _ => { let (r, g, b) = cssparser::color::parse_named_color(ident)?; SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE)) }, }) } /// Parse a CSS color using the specified [`ColorParser`] and return a new color /// value on success. pub fn parse_color_with<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); let token = input.next()?; match *token { Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes()) .map(|(r, g, b, a)| { SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a)) }), Token::Ident(ref value) => parse_color_keyword(value), Token::Function(ref name) => { let name = name.clone(); return input.parse_nested_block(|arguments| { let color_function = parse_color_function(context, name, arguments)?; if color_function.has_origin_color() { // Preserve the color as it was parsed. Ok(SpecifiedColor::ColorFunction(Box::new(color_function))) } else if let Ok(resolved) = color_function.resolve_to_absolute() { Ok(SpecifiedColor::from_absolute_color(resolved)) } else { // This will only happen when the parsed color contains errors like calc units // that cannot be resolved at parse time, but will fail when trying to resolve // them, etc. This should be rare, but for now just failing the color value // makes sense. Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } }); }, _ => Err(()), } .map_err(|()| location.new_unexpected_token_error(token.clone())) } /// Parse one of the color functions: rgba(), lab(), color(), etc. #[inline] fn parse_color_function<'i, 't>( context: &ParserContext, name: CowRcStr<'i>, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { let origin_color = parse_origin_color(context, arguments)?; let has_origin_color = origin_color.is_some(); let color = match_ignore_ascii_case! { &name, "rgb" | "rgba" => parse_rgb(context, arguments, origin_color), "hsl" | "hsla" => parse_hsl(context, arguments, origin_color), "hwb" => parse_hwb(context, arguments, origin_color), "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab), "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch), "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab), "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch), "color" => parse_color_with_color_space(context, arguments, origin_color), _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))), }?; if has_origin_color { // Validate the channels and calc expressions by trying to resolve them against // transparent. // FIXME(emilio, bug 1925572): This could avoid cloning, or be done earlier. let abs = color .map_origin_color(|_| Ok(AbsoluteColor::TRANSPARENT_BLACK)) .unwrap(); if abs.resolve_to_absolute().is_err() { return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } } arguments.expect_exhausted()?; Ok(color) } /// Parse the relative color syntax "from" syntax `from `. fn parse_origin_color<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !rcs_enabled() { return Ok(None); } // Not finding the from keyword is not an error, it just means we don't // have an origin color. if arguments .try_parse(|p| p.expect_ident_matching("from")) .is_err() { return Ok(None); } SpecifiedColor::parse(context, arguments).map(Option::Some) } #[inline] fn parse_rgb<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, ) -> Result, ParseError<'i>> { let maybe_red = parse_number_or_percentage(context, arguments, true)?; // If the first component is not "none" and is followed by a comma, then we // are parsing the legacy syntax. Legacy syntax also doesn't support an // origin color. let is_legacy_syntax = origin_color.is_none() && !maybe_red.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); Ok(if is_legacy_syntax { let (green, blue) = if maybe_red.could_be_percentage() { let green = parse_percentage(context, arguments, false)?; arguments.expect_comma()?; let blue = parse_percentage(context, arguments, false)?; (green, blue) } else { let green = parse_number(context, arguments, false)?; arguments.expect_comma()?; let blue = parse_number(context, arguments, false)?; (green, blue) }; let alpha = parse_legacy_alpha(context, arguments)?; ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha) } else { let green = parse_number_or_percentage(context, arguments, true)?; let blue = parse_number_or_percentage(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha) }) } /// Parses hsl syntax. /// /// #[inline] fn parse_hsl<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, ) -> Result, ParseError<'i>> { let hue = parse_number_or_angle(context, arguments, true)?; // If the hue is not "none" and is followed by a comma, then we are parsing // the legacy syntax. Legacy syntax also doesn't support an origin color. let is_legacy_syntax = origin_color.is_none() && !hue.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); let (saturation, lightness, alpha) = if is_legacy_syntax { let saturation = parse_percentage(context, arguments, false)?; arguments.expect_comma()?; let lightness = parse_percentage(context, arguments, false)?; let alpha = parse_legacy_alpha(context, arguments)?; (saturation, lightness, alpha) } else { let saturation = parse_number_or_percentage(context, arguments, true)?; let lightness = parse_number_or_percentage(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; (saturation, lightness, alpha) }; Ok(ColorFunction::Hsl( origin_color.into(), hue, saturation, lightness, alpha, )) } /// Parses hwb syntax. /// /// #[inline] fn parse_hwb<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, ) -> Result, ParseError<'i>> { let hue = parse_number_or_angle(context, arguments, true)?; let whiteness = parse_number_or_percentage(context, arguments, true)?; let blackness = parse_number_or_percentage(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; Ok(ColorFunction::Hwb( origin_color.into(), hue, whiteness, blackness, alpha, )) } type IntoLabFn = fn( origin: Optional, l: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Output; #[inline] fn parse_lab_like<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, into_color: IntoLabFn>, ) -> Result, ParseError<'i>> { let lightness = parse_number_or_percentage(context, arguments, true)?; let a = parse_number_or_percentage(context, arguments, true)?; let b = parse_number_or_percentage(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; Ok(into_color(origin_color.into(), lightness, a, b, alpha)) } type IntoLchFn = fn( origin: Optional, l: ColorComponent, a: ColorComponent, b: ColorComponent, alpha: ColorComponent, ) -> Output; #[inline] fn parse_lch_like<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, into_color: IntoLchFn>, ) -> Result, ParseError<'i>> { let lightness = parse_number_or_percentage(context, arguments, true)?; let chroma = parse_number_or_percentage(context, arguments, true)?; let hue = parse_number_or_angle(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; Ok(into_color( origin_color.into(), lightness, chroma, hue, alpha, )) } /// Parse the color() function. #[inline] fn parse_color_with_color_space<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, origin_color: Option, ) -> Result, ParseError<'i>> { let color_space = PredefinedColorSpace::parse(arguments)?; let c1 = parse_number_or_percentage(context, arguments, true)?; let c2 = parse_number_or_percentage(context, arguments, true)?; let c3 = parse_number_or_percentage(context, arguments, true)?; let alpha = parse_modern_alpha(context, arguments)?; Ok(ColorFunction::Color( origin_color.into(), c1, c2, c3, alpha, color_space.into(), )) } /// Either a percentage or a number. #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] #[repr(u8)] pub enum NumberOrPercentageComponent { /// ``. Number(f32), /// `` /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0. Percentage(f32), } impl NumberOrPercentageComponent { /// Return the value as a number. Percentages will be adjusted to the range /// [0..percent_basis]. pub fn to_number(&self, percentage_basis: f32) -> f32 { match *self { Self::Number(value) => value, Self::Percentage(unit_value) => unit_value * percentage_basis, } } } impl ColorComponentType for NumberOrPercentageComponent { fn from_value(value: f32) -> Self { Self::Number(value) } fn units() -> CalcUnits { CalcUnits::PERCENTAGE } fn try_from_token(token: &Token) -> Result { Ok(match *token { Token::Number { value, .. } => Self::Number(value), Token::Percentage { unit_value, .. } => Self::Percentage(unit_value), _ => { return Err(()); }, }) } fn try_from_leaf(leaf: &Leaf) -> Result { Ok(match *leaf { Leaf::Percentage(unit_value) => Self::Percentage(unit_value), Leaf::Number(value) => Self::Number(value), _ => return Err(()), }) } } /// Either an angle or a number. #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] #[repr(u8)] pub enum NumberOrAngleComponent { /// ``. Number(f32), /// `` /// The value as a number of degrees. Angle(f32), } impl NumberOrAngleComponent { /// Return the angle in degrees. `NumberOrAngle::Number` is returned as /// degrees, because it is the canonical unit. pub fn degrees(&self) -> f32 { match *self { Self::Number(value) => value, Self::Angle(degrees) => degrees, } } } impl ColorComponentType for NumberOrAngleComponent { fn from_value(value: f32) -> Self { Self::Number(value) } fn units() -> CalcUnits { CalcUnits::ANGLE } fn try_from_token(token: &Token) -> Result { Ok(match *token { Token::Number { value, .. } => Self::Number(value), Token::Dimension { value, ref unit, .. } => { let degrees = SpecifiedAngle::parse_dimension(value, unit, /* from_calc = */ false) .map(|angle| angle.degrees())?; NumberOrAngleComponent::Angle(degrees) }, _ => { return Err(()); }, }) } fn try_from_leaf(leaf: &Leaf) -> Result { Ok(match *leaf { Leaf::Angle(angle) => Self::Angle(angle.degrees()), Leaf::Number(value) => Self::Number(value), _ => return Err(()), }) } } /// The raw f32 here is for . impl ColorComponentType for f32 { fn from_value(value: f32) -> Self { value } fn units() -> CalcUnits { CalcUnits::empty() } fn try_from_token(token: &Token) -> Result { if let Token::Number { value, .. } = *token { Ok(value) } else { Err(()) } } fn try_from_leaf(leaf: &Leaf) -> Result { if let Leaf::Number(value) = *leaf { Ok(value) } else { Err(()) } } } /// Parse an `` or `` value. fn parse_number_or_angle<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { ColorComponent::parse(context, input, allow_none) } /// Parse a `` value. fn parse_percentage<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { let location = input.current_source_location(); let value = ColorComponent::::parse(context, input, allow_none)?; if !value.could_be_percentage() { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(value) } /// Parse a `` value. fn parse_number<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { let location = input.current_source_location(); let value = ColorComponent::::parse(context, input, allow_none)?; if !value.could_be_number() { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(value) } /// Parse a `` or `` value. fn parse_number_or_percentage<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_none: bool, ) -> Result, ParseError<'i>> { ColorComponent::parse(context, input, allow_none) } fn parse_legacy_alpha<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !arguments.is_exhausted() { arguments.expect_comma()?; parse_number_or_percentage(context, arguments, false) } else { Ok(ColorComponent::AlphaOmitted) } } fn parse_modern_alpha<'i, 't>( context: &ParserContext, arguments: &mut Parser<'i, 't>, ) -> Result, ParseError<'i>> { if !arguments.is_exhausted() { arguments.expect_delim('/')?; parse_number_or_percentage(context, arguments, true) } else { Ok(ColorComponent::AlphaOmitted) } } impl ColorComponent { /// Return true if the value contained inside is/can resolve to a number. /// Also returns false if the node is invalid somehow. fn could_be_number(&self) -> bool { match self { Self::None | Self::AlphaOmitted => true, Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }), Self::ChannelKeyword(_) => { // Channel keywords always resolve to numbers. true }, Self::Calc(node) => { if let Ok(unit) = node.unit() { unit.is_empty() } else { false } }, } } /// Return true if the value contained inside is/can resolve to a percentage. /// Also returns false if the node is invalid somehow. fn could_be_percentage(&self) -> bool { match self { Self::None | Self::AlphaOmitted => true, Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }), Self::ChannelKeyword(_) => { // Channel keywords always resolve to numbers. false }, Self::Calc(node) => { if let Ok(unit) = node.unit() { unit == CalcUnits::PERCENTAGE } else { false } }, } } } ================================================ FILE: style/color/to_css.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Write colors into CSS strings. use super::{ parsing::{NumberOrAngleComponent, NumberOrPercentageComponent}, AbsoluteColor, ColorFlags, ColorSpace, }; use crate::values::normalize; use cssparser::color::{clamp_unit_f32, serialize_color_alpha, OPAQUE}; use std::fmt::{self, Write}; use style_traits::{CssWriter, ToCss}; /// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and /// floating point values. struct ModernComponent<'a>(&'a Option); impl<'a> ToCss for ModernComponent<'a> { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { if let Some(value) = self.0 { if value.is_finite() { value.to_css(dest) } else if value.is_nan() { dest.write_str("calc(NaN)") } else { debug_assert!(value.is_infinite()); if value.is_sign_negative() { dest.write_str("calc(-infinity)") } else { dest.write_str("calc(infinity)") } } } else { dest.write_str("none") } } } impl ToCss for NumberOrPercentageComponent { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { use crate::values::computed::Percentage; match self { Self::Number(number) => number.to_css(dest)?, Self::Percentage(percentage) => Percentage(*percentage).to_css(dest)?, } Ok(()) } } impl ToCss for NumberOrAngleComponent { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { use crate::values::computed::Angle; match self { Self::Number(number) => number.to_css(dest)?, Self::Angle(degrees) => Angle::from_degrees(*degrees).to_css(dest)?, } Ok(()) } } impl ToCss for AbsoluteColor { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match self.color_space { ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => { // The "none" keyword is not supported in the rgb/rgba legacy syntax. let has_alpha = self.alpha != OPAQUE; dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; clamp_unit_f32(self.components.0).to_css(dest)?; dest.write_str(", ")?; clamp_unit_f32(self.components.1).to_css(dest)?; dest.write_str(", ")?; clamp_unit_f32(self.components.2).to_css(dest)?; // Legacy syntax does not allow none components. serialize_color_alpha(dest, Some(self.alpha), true)?; dest.write_char(')') }, ColorSpace::Hsl | ColorSpace::Hwb => { if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) { self.into_srgb_legacy().to_css(dest) } else { self.to_color_space(ColorSpace::Srgb).to_css(dest) } }, ColorSpace::Oklab | ColorSpace::Lab | ColorSpace::Oklch | ColorSpace::Lch => { if let ColorSpace::Oklab | ColorSpace::Oklch = self.color_space { dest.write_str("ok")?; } if let ColorSpace::Oklab | ColorSpace::Lab = self.color_space { dest.write_str("lab(")?; } else { dest.write_str("lch(")?; } ModernComponent(&self.c0()).to_css(dest)?; dest.write_char(' ')?; ModernComponent(&self.c1()).to_css(dest)?; dest.write_char(' ')?; ModernComponent(&self.c2()).to_css(dest)?; serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, _ => { #[cfg(debug_assertions)] match self.color_space { ColorSpace::Srgb => { debug_assert!( !self.flags.contains(ColorFlags::IS_LEGACY_SRGB), "legacy srgb is not a color function" ); }, ColorSpace::SrgbLinear | ColorSpace::DisplayP3 | ColorSpace::DisplayP3Linear | ColorSpace::A98Rgb | ColorSpace::ProphotoRgb | ColorSpace::Rec2020 | ColorSpace::XyzD50 | ColorSpace::XyzD65 => { // These color spaces are allowed. }, ColorSpace::Hsl | ColorSpace::Hwb | ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch => { unreachable!("other color spaces do not support color() syntax") }, }; dest.write_str("color(")?; self.color_space.to_css(dest)?; dest.write_char(' ')?; ModernComponent(&self.c0()).to_css(dest)?; dest.write_char(' ')?; ModernComponent(&self.c1()).to_css(dest)?; dest.write_char(' ')?; ModernComponent(&self.c2()).to_css(dest)?; serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, } } } impl AbsoluteColor { /// Write a string to `dest` that represents a color as an author would /// enter it. /// NOTE: The format of the output is NOT according to any specification, /// but makes assumptions about the best ways that authors would want to /// enter color values in style sheets, devtools, etc. pub fn write_author_preferred_value(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { macro_rules! precision { ($v:expr) => {{ ($v * 100.0).round() / 100.0 }}; } macro_rules! number { ($c:expr) => {{ if let Some(v) = $c.map(normalize) { precision!(v).to_css(dest)?; } else { write!(dest, "none")?; } }}; } macro_rules! percentage { ($c:expr) => {{ if let Some(v) = $c.map(normalize) { precision!(v).to_css(dest)?; dest.write_char('%')?; } else { write!(dest, "none")?; } }}; } macro_rules! unit_percentage { ($c:expr) => {{ if let Some(v) = $c.map(normalize) { precision!(v * 100.0).to_css(dest)?; dest.write_char('%')?; } else { write!(dest, "none")?; } }}; } macro_rules! angle { ($c:expr) => {{ if let Some(v) = $c.map(normalize) { precision!(v).to_css(dest)?; dest.write_str("deg")?; } else { write!(dest, "none")?; } }}; } match self.color_space { ColorSpace::Srgb => { write!(dest, "rgb(")?; unit_percentage!(self.c0()); dest.write_char(' ')?; unit_percentage!(self.c1()); dest.write_char(' ')?; unit_percentage!(self.c2()); serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, ColorSpace::Hsl | ColorSpace::Hwb => { dest.write_str(if self.color_space == ColorSpace::Hsl { "hsl(" } else { "hwb(" })?; angle!(self.c0()); dest.write_char(' ')?; percentage!(self.c1()); dest.write_char(' ')?; percentage!(self.c2()); serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, ColorSpace::Lab | ColorSpace::Oklab => { if self.color_space == ColorSpace::Oklab { dest.write_str("ok")?; } dest.write_str("lab(")?; if self.color_space == ColorSpace::Lab { percentage!(self.c0()) } else { unit_percentage!(self.c0()) } dest.write_char(' ')?; number!(self.c1()); dest.write_char(' ')?; number!(self.c2()); serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, ColorSpace::Lch | ColorSpace::Oklch => { if self.color_space == ColorSpace::Oklch { dest.write_str("ok")?; } dest.write_str("lch(")?; number!(self.c0()); dest.write_char(' ')?; number!(self.c1()); dest.write_char(' ')?; angle!(self.c2()); serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, ColorSpace::SrgbLinear | ColorSpace::DisplayP3 | ColorSpace::DisplayP3Linear | ColorSpace::A98Rgb | ColorSpace::ProphotoRgb | ColorSpace::Rec2020 | ColorSpace::XyzD50 | ColorSpace::XyzD65 => { dest.write_str("color(")?; self.color_space.to_css(dest)?; dest.write_char(' ')?; number!(self.c0()); dest.write_char(' ')?; number!(self.c1()); dest.write_char(' ')?; number!(self.c2()); serialize_color_alpha(dest, self.alpha(), false)?; dest.write_char(')') }, } } } ================================================ FILE: style/context.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The context within which style is calculated. #[cfg(feature = "servo")] use crate::animation::DocumentAnimationSet; use crate::bloom::StyleBloom; use crate::computed_value_flags::ComputedValueFlags; use crate::data::{EagerPseudoStyles, ElementData}; use crate::derives::*; use crate::dom::{SendElement, TElement}; #[cfg(feature = "gecko")] use crate::gecko_bindings::structs; use crate::parallel::{STACK_SAFETY_MARGIN_KB, STYLE_THREAD_STACK_SIZE_KB}; use crate::properties::ComputedValues; #[cfg(feature = "servo")] use crate::properties::PropertyId; use crate::rule_cache::RuleCache; use crate::rule_tree::{RuleCascadeFlags, StrongRuleNode}; use crate::selector_parser::{SnapshotMap, EAGER_PSEUDO_COUNT}; use crate::shared_lock::StylesheetGuards; use crate::sharing::StyleSharingCache; use crate::stylist::Stylist; use crate::thread_state::{self, ThreadState}; use crate::traversal::DomTraversal; use crate::traversal_flags::TraversalFlags; use app_units::Au; use euclid::default::Size2D; use euclid::Scale; #[cfg(feature = "servo")] use rustc_hash::FxHashMap; use selectors::context::SelectorCaches; #[cfg(feature = "gecko")] use servo_arc::Arc; use std::fmt; use std::ops; use std::time::{Duration, Instant}; use style_traits::CSSPixel; use style_traits::DevicePixel; #[cfg(feature = "servo")] use style_traits::SpeculativePainter; #[cfg(feature = "servo")] use stylo_atoms::Atom; pub use selectors::matching::QuirksMode; /// A global options structure for the style system. We use this instead of /// opts to abstract across Gecko and Servo. #[derive(Clone)] pub struct StyleSystemOptions { /// Whether the style sharing cache is disabled. pub disable_style_sharing_cache: bool, /// Whether we should dump statistics about the style system. pub dump_style_statistics: bool, /// The minimum number of elements that must be traversed to trigger a dump /// of style statistics. pub style_statistics_threshold: usize, } #[cfg(feature = "gecko")] fn get_env_bool(name: &str) -> bool { use std::env; match env::var(name) { Ok(s) => !s.is_empty(), Err(_) => false, } } const DEFAULT_STATISTICS_THRESHOLD: usize = 50; #[cfg(feature = "gecko")] fn get_env_usize(name: &str) -> Option { use std::env; env::var(name).ok().map(|s| { s.parse::() .expect("Couldn't parse environmental variable as usize") }) } /// A global variable holding the state of /// `StyleSystemOptions::default().disable_style_sharing_cache`. /// See [#22854](https://github.com/servo/servo/issues/22854). #[cfg(feature = "servo")] pub static DEFAULT_DISABLE_STYLE_SHARING_CACHE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); /// A global variable holding the state of /// `StyleSystemOptions::default().dump_style_statistics`. /// See [#22854](https://github.com/servo/servo/issues/22854). #[cfg(feature = "servo")] pub static DEFAULT_DUMP_STYLE_STATISTICS: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); impl Default for StyleSystemOptions { #[cfg(feature = "servo")] fn default() -> Self { use std::sync::atomic::Ordering; StyleSystemOptions { disable_style_sharing_cache: DEFAULT_DISABLE_STYLE_SHARING_CACHE .load(Ordering::Relaxed), dump_style_statistics: DEFAULT_DUMP_STYLE_STATISTICS.load(Ordering::Relaxed), style_statistics_threshold: DEFAULT_STATISTICS_THRESHOLD, } } #[cfg(feature = "gecko")] fn default() -> Self { StyleSystemOptions { disable_style_sharing_cache: get_env_bool("DISABLE_STYLE_SHARING_CACHE"), dump_style_statistics: get_env_bool("DUMP_STYLE_STATISTICS"), style_statistics_threshold: get_env_usize("STYLE_STATISTICS_THRESHOLD") .unwrap_or(DEFAULT_STATISTICS_THRESHOLD), } } } /// A shared style context. /// /// There's exactly one of these during a given restyle traversal, and it's /// shared among the worker threads. pub struct SharedStyleContext<'a> { /// The CSS selector stylist. pub stylist: &'a Stylist, /// Whether visited styles are enabled. /// /// They may be disabled when Gecko's pref layout.css.visited_links_enabled /// is false, or when in private browsing mode. pub visited_styles_enabled: bool, /// Configuration options. pub options: StyleSystemOptions, /// Guards for pre-acquired locks pub guards: StylesheetGuards<'a>, /// The current time for transitions and animations. This is needed to ensure /// a consistent sampling time and also to adjust the time for testing. pub current_time_for_animations: f64, /// Flags controlling how we traverse the tree. pub traversal_flags: TraversalFlags, /// A map with our snapshots in order to handle restyle hints. pub snapshot_map: &'a SnapshotMap, /// The state of all animations for our styled elements. #[cfg(feature = "servo")] pub animations: DocumentAnimationSet, /// Paint worklets #[cfg(feature = "servo")] pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters, } impl<'a> SharedStyleContext<'a> { /// Return a suitable viewport size in order to be used for viewport units. pub fn viewport_size(&self) -> Size2D { self.stylist.device().au_viewport_size() } /// The device pixel ratio pub fn device_pixel_ratio(&self) -> Scale { self.stylist.device().device_pixel_ratio() } /// The quirks mode of the document. pub fn quirks_mode(&self) -> QuirksMode { self.stylist.quirks_mode() } } /// The structure holds various intermediate inputs that are eventually used by /// by the cascade. /// /// The matching and cascading process stores them in this format temporarily /// within the `CurrentElementInfo`. At the end of the cascade, they are folded /// down into the main `ComputedValues` to reduce memory usage per element while /// still remaining accessible. #[derive(Clone, Debug, Default)] pub struct CascadeInputs { /// The rule node representing the ordered list of rules matched for this /// node. pub rules: Option, /// The rule node representing the ordered list of rules matched for this /// node if visited, only computed if there's a relevant link for this /// element. A element's "relevant link" is the element being matched if it /// is a link or the nearest ancestor link. pub visited_rules: Option, /// The set of flags from container queries that we need for invalidation. pub flags: ComputedValueFlags, /// The set of RuleCascadeFlags to include in the cascade. pub included_cascade_flags: RuleCascadeFlags, } impl CascadeInputs { /// Construct inputs from previous cascade results, if any. pub fn new_from_style(style: &ComputedValues) -> Self { Self { rules: style.rules.clone(), visited_rules: style.visited_style().and_then(|v| v.rules.clone()), flags: style.flags.for_cascade_inputs(), included_cascade_flags: RuleCascadeFlags::empty(), } } } /// A list of cascade inputs for eagerly-cascaded pseudo-elements. /// The list is stored inline. #[derive(Debug)] pub struct EagerPseudoCascadeInputs(Option<[Option; EAGER_PSEUDO_COUNT]>); // Manually implement `Clone` here because the derived impl of `Clone` for // array types assumes the value inside is `Copy`. impl Clone for EagerPseudoCascadeInputs { fn clone(&self) -> Self { if self.0.is_none() { return EagerPseudoCascadeInputs(None); } let self_inputs = self.0.as_ref().unwrap(); let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); for i in 0..EAGER_PSEUDO_COUNT { inputs[i] = self_inputs[i].clone(); } EagerPseudoCascadeInputs(Some(inputs)) } } impl EagerPseudoCascadeInputs { /// Construct inputs from previous cascade results, if any. fn new_from_style(styles: &EagerPseudoStyles) -> Self { EagerPseudoCascadeInputs(styles.as_optional_array().map(|styles| { let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); for i in 0..EAGER_PSEUDO_COUNT { inputs[i] = styles[i].as_ref().map(|s| CascadeInputs::new_from_style(s)); } inputs })) } /// Returns the list of rules, if they exist. pub fn into_array(self) -> Option<[Option; EAGER_PSEUDO_COUNT]> { self.0 } } /// The cascade inputs associated with a node, including those for any /// pseudo-elements. /// /// The matching and cascading process stores them in this format temporarily /// within the `CurrentElementInfo`. At the end of the cascade, they are folded /// down into the main `ComputedValues` to reduce memory usage per element while /// still remaining accessible. #[derive(Clone, Debug)] pub struct ElementCascadeInputs { /// The element's cascade inputs. pub primary: CascadeInputs, /// A list of the inputs for the element's eagerly-cascaded pseudo-elements. pub pseudos: EagerPseudoCascadeInputs, } impl ElementCascadeInputs { /// Construct inputs from previous cascade results, if any. #[inline] pub fn new_from_element_data(data: &ElementData) -> Self { debug_assert!(data.has_styles()); ElementCascadeInputs { primary: CascadeInputs::new_from_style(data.styles.primary()), pseudos: EagerPseudoCascadeInputs::new_from_style(&data.styles.pseudos), } } } /// Statistics gathered during the traversal. We gather statistics on each /// thread and then combine them after the threads join via the Add /// implementation below. #[derive(AddAssign, Clone, Default)] pub struct PerThreadTraversalStatistics { /// The total number of elements traversed. pub elements_traversed: u32, /// The number of elements where has_styles() went from false to true. pub elements_styled: u32, /// The number of elements for which we performed selector matching. pub elements_matched: u32, /// The number of cache hits from the StyleSharingCache. pub styles_shared: u32, /// The number of styles reused via rule node comparison from the /// StyleSharingCache. pub styles_reused: u32, } /// Statistics gathered during the traversal plus some information from /// other sources including stylist. #[derive(Default)] pub struct TraversalStatistics { /// Aggregated statistics gathered during the traversal. pub aggregated: PerThreadTraversalStatistics, /// The number of selectors in the stylist. pub selectors: u32, /// The number of revalidation selectors. pub revalidation_selectors: u32, /// The number of state/attr dependencies in the dependency set. pub dependency_selectors: u32, /// The number of declarations in the stylist. pub declarations: u32, /// The number of times the stylist was rebuilt. pub stylist_rebuilds: u32, /// Time spent in the traversal, in milliseconds. pub traversal_time: Duration, /// Whether this was a parallel traversal. pub is_parallel: bool, /// Whether this is a "large" traversal. pub is_large: bool, } /// Format the statistics in a way that the performance test harness understands. /// See https://bugzilla.mozilla.org/show_bug.cgi?id=1331856#c2 impl fmt::Display for TraversalStatistics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "[PERF] perf block start")?; writeln!( f, "[PERF],traversal,{}", if self.is_parallel { "parallel" } else { "sequential" } )?; writeln!( f, "[PERF],elements_traversed,{}", self.aggregated.elements_traversed )?; writeln!( f, "[PERF],elements_styled,{}", self.aggregated.elements_styled )?; writeln!( f, "[PERF],elements_matched,{}", self.aggregated.elements_matched )?; writeln!(f, "[PERF],styles_shared,{}", self.aggregated.styles_shared)?; writeln!(f, "[PERF],styles_reused,{}", self.aggregated.styles_reused)?; writeln!(f, "[PERF],selectors,{}", self.selectors)?; writeln!( f, "[PERF],revalidation_selectors,{}", self.revalidation_selectors )?; writeln!( f, "[PERF],dependency_selectors,{}", self.dependency_selectors )?; writeln!(f, "[PERF],declarations,{}", self.declarations)?; writeln!(f, "[PERF],stylist_rebuilds,{}", self.stylist_rebuilds)?; writeln!( f, "[PERF],traversal_time_ms,{}", self.traversal_time.as_secs_f64() * 1000. )?; writeln!(f, "[PERF] perf block end") } } impl TraversalStatistics { /// Generate complete traversal statistics. /// /// The traversal time is computed given the start time in seconds. pub fn new( aggregated: PerThreadTraversalStatistics, traversal: &D, parallel: bool, start: Instant, ) -> TraversalStatistics where E: TElement, D: DomTraversal, { let threshold = traversal .shared_context() .options .style_statistics_threshold; let stylist = traversal.shared_context().stylist; let is_large = aggregated.elements_traversed as usize >= threshold; TraversalStatistics { aggregated, selectors: stylist.num_selectors() as u32, revalidation_selectors: stylist.num_revalidation_selectors() as u32, dependency_selectors: stylist.num_invalidations() as u32, declarations: stylist.num_declarations() as u32, stylist_rebuilds: stylist.num_rebuilds() as u32, traversal_time: Instant::now() - start, is_parallel: parallel, is_large, } } } #[cfg(feature = "gecko")] bitflags! { /// Represents which tasks are performed in a SequentialTask of /// UpdateAnimations which is a result of normal restyle. pub struct UpdateAnimationsTasks: u8 { /// Update CSS Animations. const CSS_ANIMATIONS = structs::UpdateAnimationsTasks_CSSAnimations; /// Update CSS Transitions. const CSS_TRANSITIONS = structs::UpdateAnimationsTasks_CSSTransitions; /// Update effect properties. const EFFECT_PROPERTIES = structs::UpdateAnimationsTasks_EffectProperties; /// Update animation cacade results for animations running on the compositor. const CASCADE_RESULTS = structs::UpdateAnimationsTasks_CascadeResults; /// Display property was changed from none. /// Script animations keep alive on display:none elements, so we need to trigger /// the second animation restyles for the script animations in the case where /// the display property was changed from 'none' to others. const DISPLAY_CHANGED_FROM_NONE = structs::UpdateAnimationsTasks_DisplayChangedFromNone; /// Update CSS named scroll progress timelines. const SCROLL_TIMELINES = structs::UpdateAnimationsTasks_ScrollTimelines; /// Update CSS named view progress timelines. const VIEW_TIMELINES = structs::UpdateAnimationsTasks_ViewTimelines; /// Update CSS timeline scopes, which affect visibility of both scroll and view timelines. const TIMELINE_SCOPES = structs::UpdateAnimationsTasks_TimelineScopes; } } /// A task to be run in sequential mode on the parent (non-worker) thread. This /// is used by the style system to queue up work which is not safe to do during /// the parallel traversal. pub enum SequentialTask { /// Entry to avoid an unused type parameter error on servo. Unused(SendElement), /// Performs one of a number of possible tasks related to updating /// animations based on the |tasks| field. These include updating CSS /// animations/transitions that changed as part of the non-animation style /// traversal, and updating the computed effect properties. #[cfg(feature = "gecko")] UpdateAnimations { /// The target element or pseudo-element. el: SendElement, /// The before-change style for transitions. We use before-change style /// as the initial value of its Keyframe. Required if |tasks| includes /// CSSTransitions. before_change_style: Option>, /// The tasks which are performed in this SequentialTask. tasks: UpdateAnimationsTasks, }, } impl SequentialTask { /// Executes this task. pub fn execute(self) { use self::SequentialTask::*; debug_assert!(thread_state::get().contains(ThreadState::LAYOUT)); match self { Unused(_) => unreachable!(), #[cfg(feature = "gecko")] UpdateAnimations { el, before_change_style, tasks, } => { el.update_animations(before_change_style, tasks); }, } } /// Creates a task to update various animation-related state on a given /// (pseudo-)element. #[cfg(feature = "gecko")] pub fn update_animations( el: E, before_change_style: Option>, tasks: UpdateAnimationsTasks, ) -> Self { use self::SequentialTask::*; UpdateAnimations { el: unsafe { SendElement::new(el) }, before_change_style, tasks, } } } /// A list of SequentialTasks that get executed on Drop. pub struct SequentialTaskList(Vec>) where E: TElement; impl ops::Deref for SequentialTaskList where E: TElement, { type Target = Vec>; fn deref(&self) -> &Self::Target { &self.0 } } impl ops::DerefMut for SequentialTaskList where E: TElement, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Drop for SequentialTaskList where E: TElement, { fn drop(&mut self) { debug_assert!(thread_state::get().contains(ThreadState::LAYOUT)); for task in self.0.drain(..) { task.execute() } } } /// A helper type for stack limit checking. This assumes that stacks grow /// down, which is true for all non-ancient CPU architectures. pub struct StackLimitChecker { lower_limit: usize, } impl StackLimitChecker { /// Create a new limit checker, for this thread, allowing further use /// of up to |stack_size| bytes beyond (below) the current stack pointer. #[inline(never)] pub fn new(stack_size_limit: usize) -> Self { StackLimitChecker { lower_limit: StackLimitChecker::get_sp() - stack_size_limit, } } /// Checks whether the previously stored stack limit has now been exceeded. #[inline(never)] pub fn limit_exceeded(&self) -> bool { let curr_sp = StackLimitChecker::get_sp(); // Do some sanity-checking to ensure that our invariants hold, even in // the case where we've exceeded the soft limit. // // The correctness of depends on the assumption that no stack wraps // around the end of the address space. if cfg!(debug_assertions) { // Compute the actual bottom of the stack by subtracting our safety // margin from our soft limit. Note that this will be slightly below // the actual bottom of the stack, because there are a few initial // frames on the stack before we do the measurement that computes // the limit. let stack_bottom = self.lower_limit - STACK_SAFETY_MARGIN_KB * 1024; // The bottom of the stack should be below the current sp. If it // isn't, that means we've either waited too long to check the limit // and burned through our safety margin (in which case we probably // would have segfaulted by now), or we're using a limit computed for // a different thread. debug_assert!(stack_bottom < curr_sp); // Compute the distance between the current sp and the bottom of // the stack, and compare it against the current stack. It should be // no further from us than the total stack size. We allow some slop // to handle the fact that stack_bottom is a bit further than the // bottom of the stack, as discussed above. let distance_to_stack_bottom = curr_sp - stack_bottom; let max_allowable_distance = (STYLE_THREAD_STACK_SIZE_KB + 10) * 1024; debug_assert!(distance_to_stack_bottom <= max_allowable_distance); } // The actual bounds check. curr_sp <= self.lower_limit } // Technically, rustc can optimize this away, but shouldn't for now. // We should fix this once black_box is stable. #[inline(always)] fn get_sp() -> usize { let mut foo: usize = 42; (&mut foo as *mut usize) as usize } } /// A thread-local style context. /// /// This context contains data that needs to be used during restyling, but is /// not required to be unique among worker threads, so we create one per worker /// thread in order to be able to mutate it without locking. pub struct ThreadLocalStyleContext { /// A cache to share style among siblings. pub sharing_cache: StyleSharingCache, /// A cache from matched properties to elements that match those. pub rule_cache: RuleCache, /// The bloom filter used to fast-reject selector-matching. pub bloom_filter: StyleBloom, /// A set of tasks to be run (on the parent thread) in sequential mode after /// the rest of the styling is complete. This is useful for /// infrequently-needed non-threadsafe operations. /// /// It's important that goes after the style sharing cache and the bloom /// filter, to ensure they're dropped before we execute the tasks, which /// could create another ThreadLocalStyleContext for style computation. pub tasks: SequentialTaskList, /// Statistics about the traversal. pub statistics: PerThreadTraversalStatistics, /// A checker used to ensure that parallel.rs does not recurse indefinitely /// even on arbitrarily deep trees. See Gecko bug 1376883. pub stack_limit_checker: StackLimitChecker, /// Collection of caches (And cache-likes) for speeding up expensive selector matches. pub selector_caches: SelectorCaches, } impl ThreadLocalStyleContext { /// Creates a new `ThreadLocalStyleContext` pub fn new() -> Self { ThreadLocalStyleContext { sharing_cache: StyleSharingCache::new(), rule_cache: RuleCache::new(), bloom_filter: StyleBloom::new(), tasks: SequentialTaskList(Vec::new()), statistics: PerThreadTraversalStatistics::default(), stack_limit_checker: StackLimitChecker::new( (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, ), selector_caches: SelectorCaches::default(), } } } /// A `StyleContext` is just a simple container for a immutable reference to a /// shared style context, and a mutable reference to a local one. pub struct StyleContext<'a, E: TElement + 'a> { /// The shared style context reference. pub shared: &'a SharedStyleContext<'a>, /// The thread-local style context (mutable) reference. pub thread_local: &'a mut ThreadLocalStyleContext, } /// A registered painter #[cfg(feature = "servo")] pub trait RegisteredSpeculativePainter: SpeculativePainter { /// The name it was registered with fn name(&self) -> Atom; /// The properties it was registered with fn properties(&self) -> &FxHashMap; } /// A set of registered painters #[cfg(feature = "servo")] pub trait RegisteredSpeculativePainters: Sync { /// Look up a speculative painter fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter>; } ================================================ FILE: style/counter_style/mod.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The [`@counter-style`][counter-style] at-rule. //! //! [counter-style]: https://drafts.csswg.org/css-counter-styles/ use crate::derives::*; use crate::error_reporting::ContextualParseError; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; use crate::values::specified::Integer; use crate::values::{AtomString, CustomIdent}; use crate::Atom; use cssparser::{ ascii_case_insensitive_phf_map, match_ignore_ascii_case, CowRcStr, Parser, RuleBodyParser, SourceLocation, Token, }; use std::fmt::{self, Write}; use std::mem; use std::num::Wrapping; use style_traits::{ Comma, CssStringWriter, CssWriter, KeywordsCollectFn, OneOrMoreSeparated, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, }; pub use crate::properties::counter_style::{DescriptorId, DescriptorParser, Descriptors}; /// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type #[allow(missing_docs)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum SymbolsType { Cyclic, Numeric, Alphabetic, Symbolic, Fixed, } /// /// /// Note that 'none' is not a valid name, but we include this (along with String) for space /// efficiency when storing list-style-type. #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum CounterStyle { /// The 'none' value. None, /// `` Name(CustomIdent), /// `symbols()` #[css(function)] Symbols { /// The , or symbolic if not specified. #[css(skip_if = "is_symbolic")] ty: SymbolsType, /// The actual symbols. symbols: Symbols, }, /// A single string value, useful for ``. String(AtomString), } #[inline] fn is_symbolic(symbols_type: &SymbolsType) -> bool { *symbols_type == SymbolsType::Symbolic } impl CounterStyle { /// disc value pub fn disc() -> Self { CounterStyle::Name(CustomIdent(atom!("disc"))) } /// decimal value pub fn decimal() -> Self { CounterStyle::Name(CustomIdent(atom!("decimal"))) } /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`) #[inline] pub fn is_bullet(&self) -> bool { match self { CounterStyle::Name(CustomIdent(ref name)) => { name == &atom!("disc") || name == &atom!("circle") || name == &atom!("square") || name == &atom!("disclosure-closed") || name == &atom!("disclosure-open") }, _ => false, } } } bitflags! { #[derive(Clone, Copy)] /// Flags to control parsing of counter styles. pub struct CounterStyleParsingFlags: u8 { /// Whether `none` is allowed. const ALLOW_NONE = 1 << 0; /// Whether a bare string is allowed. const ALLOW_STRING = 1 << 1; } } impl CounterStyle { /// Parse a counter style, and optionally none|string (for list-style-type). pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, flags: CounterStyleParsingFlags, ) -> Result> { use self::CounterStyleParsingFlags as Flags; let location = input.current_source_location(); match input.next()? { Token::QuotedString(ref string) if flags.intersects(Flags::ALLOW_STRING) => { Ok(Self::String(AtomString::from(string.as_ref()))) }, Token::Ident(ref ident) => { if flags.intersects(Flags::ALLOW_NONE) && ident.eq_ignore_ascii_case("none") { return Ok(Self::None); } Ok(Self::Name(counter_style_name_from_ident(ident, location)?)) }, Token::Function(ref name) if name.eq_ignore_ascii_case("symbols") => { input.parse_nested_block(|input| { let symbols_type = input .try_parse(SymbolsType::parse) .unwrap_or(SymbolsType::Symbolic); let symbols = Symbols::parse(context, input)?; // There must be at least two symbols for alphabetic or // numeric system. if (symbols_type == SymbolsType::Alphabetic || symbols_type == SymbolsType::Numeric) && symbols.0.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // Identifier is not allowed in symbols() function. if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Self::Symbols { ty: symbols_type, symbols, }) }) }, t => Err(location.new_unexpected_token_error(t.clone())), } } } impl SpecifiedValueInfo for CounterStyle { fn collect_completion_keywords(f: KeywordsCollectFn) { // XXX The best approach for implementing this is probably // having a CounterStyleName type wrapping CustomIdent, and // put the predefined list for that type in counter_style mod. // But that's a non-trivial change itself, so we use a simpler // approach here. macro_rules! predefined { ($($name:expr,)+) => { f(&["symbols", "none", $($name,)+]) } } include!("predefined.rs"); } } fn parse_counter_style_name<'i>(input: &mut Parser<'i, '_>) -> Result> { let location = input.current_source_location(); let ident = input.expect_ident()?; counter_style_name_from_ident(ident, location) } /// This allows the reserved counter style names "decimal" and "disc". fn counter_style_name_from_ident<'i>( ident: &CowRcStr<'i>, location: SourceLocation, ) -> Result> { macro_rules! predefined { ($($name: tt,)+) => {{ ascii_case_insensitive_phf_map! { predefined -> Atom = { $( $name => atom!($name), )+ } } // This effectively performs case normalization only on predefined names. if let Some(lower_case) = predefined::get(&ident) { Ok(CustomIdent(lower_case.clone())) } else { // none is always an invalid value. CustomIdent::from_ident(location, ident, &["none"]) } }} } include!("predefined.rs") } fn is_valid_name_definition(ident: &CustomIdent) -> bool { ident.0 != atom!("decimal") && ident.0 != atom!("disc") && ident.0 != atom!("circle") && ident.0 != atom!("square") && ident.0 != atom!("disclosure-closed") && ident.0 != atom!("disclosure-open") } /// Parse the prelude of an @counter-style rule pub fn parse_counter_style_name_definition<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { parse_counter_style_name(input).and_then(|ident| { if !is_valid_name_definition(&ident) { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(ident) } }) } /// A @counter-style rule #[derive(Clone, Debug, ToShmem)] pub struct CounterStyleRule { name: CustomIdent, generation: Wrapping, descriptors: Descriptors, /// The parser location of the @counter-style rule. pub source_location: SourceLocation, } /// Parse the body (inside `{}`) of an @counter-style rule pub fn parse_counter_style_body<'i, 't>( name: CustomIdent, context: &ParserContext, input: &mut Parser<'i, 't>, location: SourceLocation, ) -> Result> { let start = input.current_source_location(); let mut rule = CounterStyleRule::empty(name, location); { let mut parser = DescriptorParser { context, descriptors: &mut rule.descriptors, }; let mut iter = RuleBodyParser::new(input, &mut parser); while let Some(declaration) = iter.next() { if let Err((error, slice)) = declaration { let location = error.location; let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration( slice, error, ); context.log_css_error(location, error) } } } let error = match *rule.resolved_system() { ref system @ System::Cyclic | ref system @ System::Fixed { .. } | ref system @ System::Symbolic | ref system @ System::Alphabetic | ref system @ System::Numeric if rule.descriptors.symbols.is_none() => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleWithoutSymbols( system, )) }, ref system @ System::Alphabetic | ref system @ System::Numeric if rule.descriptors.symbols.as_ref().unwrap().0.len() < 2 => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols( system, )) }, System::Additive if rule.descriptors.additive_symbols.is_none() => { Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) }, System::Extends(_) if rule.descriptors.symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) }, System::Extends(_) if rule.descriptors.additive_symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) }, _ => None, }; if let Some(error) = error { context.log_css_error(start, error); Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(rule) } } impl ToCssWithGuard for CounterStyleRule { fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { dest.write_str("@counter-style ")?; self.name.to_css(&mut CssWriter::new(dest))?; dest.write_str(" { ")?; self.descriptors.to_css(&mut CssWriter::new(dest))?; dest.write_char('}') } } // Implements the special checkers for some setters. // See impl CounterStyleRule { fn empty(name: CustomIdent, source_location: SourceLocation) -> Self { Self { name: name, generation: Wrapping(0), descriptors: Descriptors::default(), source_location, } } /// Expose the descriptors as read-only, since we rely on updating our generation when they /// change, and some setters need extra validation. pub fn descriptors(&self) -> &Descriptors { &self.descriptors } /// Set a descriptor, with the relevant validation. returns whether the descriptor changed. pub fn set_descriptor<'i>( &mut self, id: DescriptorId, context: &ParserContext, input: &mut Parser<'i, '_>, ) -> Result> { // Some descriptors need a couple extra checks, deal with them specially. // TODO(emilio): Remove these, see https://github.com/w3c/csswg-drafts/issues/5717 if id == DescriptorId::AdditiveSymbols && matches!(*self.resolved_system(), System::Extends(..)) { // No additive symbols should be set for extends system. return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let changed = match id { DescriptorId::System => { let system = input.parse_entirely(|i| System::parse(context, i))?; if !self.check_system(&system) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let new = Some(system); if self.descriptors.system == new { return Ok(false); } self.descriptors.system = new; true }, DescriptorId::Symbols => { let symbols = input.parse_entirely(|i| Symbols::parse(context, i))?; if !self.check_symbols(&symbols) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let new = Some(symbols); if self.descriptors.symbols == new { return Ok(false); } self.descriptors.symbols = new; true }, _ => self.descriptors.set(id, context, input)?, }; if changed { self.generation += Wrapping(1); } Ok(changed) } /// Check that the system is effectively not changed. Only params of system descriptor is /// changeable. fn check_system(&self, value: &System) -> bool { mem::discriminant(self.resolved_system()) == mem::discriminant(value) } fn check_symbols(&self, value: &Symbols) -> bool { match *self.resolved_system() { // These two systems require at least two symbols. System::Numeric | System::Alphabetic => value.0.len() >= 2, // No symbols should be set for extends system. System::Extends(_) => false, _ => true, } } /// Get the name of the counter style rule. pub fn name(&self) -> &CustomIdent { &self.name } /// Set the name of the counter style rule. Caller must ensure that /// the name is valid. pub fn set_name(&mut self, name: CustomIdent) { debug_assert!(is_valid_name_definition(&name)); self.name = name; } /// Get the current generation of the counter style rule. pub fn generation(&self) -> u32 { self.generation.0 } /// Get the system of this counter style rule, default to /// `symbolic` if not specified. pub fn resolved_system(&self) -> &System { match self.descriptors.system { Some(ref system) => system, None => &System::Symbolic, } } } /// #[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] pub enum System { /// 'cyclic' Cyclic, /// 'numeric' Numeric, /// 'alphabetic' Alphabetic, /// 'symbolic' Symbolic, /// 'additive' Additive, /// 'fixed ?' Fixed { /// '?' first_symbol_value: Option, }, /// 'extends ' Extends(CustomIdent), } impl Parse for System { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { try_match_ident_ignore_ascii_case! { input, "cyclic" => Ok(System::Cyclic), "numeric" => Ok(System::Numeric), "alphabetic" => Ok(System::Alphabetic), "symbolic" => Ok(System::Symbolic), "additive" => Ok(System::Additive), "fixed" => { let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok(); Ok(System::Fixed { first_symbol_value }) }, "extends" => { let other = parse_counter_style_name(input)?; Ok(System::Extends(other)) }, } } } impl ToCss for System { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match *self { System::Cyclic => dest.write_str("cyclic"), System::Numeric => dest.write_str("numeric"), System::Alphabetic => dest.write_str("alphabetic"), System::Symbolic => dest.write_str("symbolic"), System::Additive => dest.write_str("additive"), System::Fixed { first_symbol_value } => { if let Some(value) = first_symbol_value { dest.write_str("fixed ")?; value.to_css(dest) } else { dest.write_str("fixed") } }, System::Extends(ref other) => { dest.write_str("extends ")?; other.to_css(dest) }, } } } /// #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, )] #[repr(u8)] pub enum Symbol { /// String(crate::OwnedStr), /// Ident(CustomIdent), // Not implemented: // /// // Image(Image), } impl Parse for Symbol { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); match *input.next()? { Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())), Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)), ref t => Err(location.new_unexpected_token_error(t.clone())), } } } impl Symbol { /// Returns whether this symbol is allowed in symbols() function. pub fn is_allowed_in_symbols(&self) -> bool { match self { // Identifier is not allowed. &Symbol::Ident(_) => false, _ => true, } } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Negative(pub Symbol, pub Option); impl Parse for Negative { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(Negative( Symbol::parse(context, input)?, input.try_parse(|input| Symbol::parse(context, input)).ok(), )) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct CounterRange { /// The start of the range. pub start: CounterBound, /// The end of the range. pub end: CounterBound, } /// /// /// Empty represents 'auto' #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] #[css(comma)] pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice); /// A bound found in `CounterRanges`. #[derive(Clone, Copy, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub enum CounterBound { /// An integer bound. Integer(Integer), /// The infinite bound. Infinite, } impl Parse for CounterRanges { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|input| input.expect_ident_matching("auto")) .is_ok() { return Ok(CounterRanges(Default::default())); } let ranges = input.parse_comma_separated(|input| { let start = parse_bound(context, input)?; let end = parse_bound(context, input)?; if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) { if start > end { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } } Ok(CounterRange { start, end }) })?; Ok(CounterRanges(ranges.into())) } } fn parse_bound<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) { return Ok(CounterBound::Integer(integer)); } input.expect_ident_matching("infinite")?; Ok(CounterBound::Infinite) } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Pad(pub Integer, pub Symbol); impl Parse for Pad { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let pad_with = input.try_parse(|input| Symbol::parse(context, input)); let min_length = Integer::parse_non_negative(context, input)?; let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; Ok(Pad(min_length, pad_with)) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Fallback(pub CustomIdent); impl Parse for Fallback { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(Fallback(parse_counter_style_name(input)?)) } } /// #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, )] #[repr(C)] pub struct Symbols( #[css(iterable)] #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice, ); impl Parse for Symbols { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut symbols = smallvec::SmallVec::<[_; 5]>::new(); while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) { symbols.push(s); } if symbols.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Symbols(crate::ArcSlice::from_iter(symbols.drain(..)))) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] #[css(comma)] pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice); impl Parse for AdditiveSymbols { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let tuples = Vec::::parse(context, input)?; // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 if tuples .windows(2) .any(|window| window[0].weight <= window[1].weight) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(AdditiveSymbols(tuples.into())) } } /// && #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct AdditiveTuple { /// pub weight: Integer, /// pub symbol: Symbol, } impl OneOrMoreSeparated for AdditiveTuple { type S = Comma; } impl Parse for AdditiveTuple { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let symbol = input.try_parse(|input| Symbol::parse(context, input)); let weight = Integer::parse_non_negative(context, input)?; let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; Ok(Self { weight, symbol }) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, PartialEq, ToShmem)] pub enum SpeakAs { /// auto Auto, /// bullets Bullets, /// numbers Numbers, /// words Words, // /// spell-out, not supported, see bug 1024178 // SpellOut, /// Other(CustomIdent), } impl Parse for SpeakAs { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut is_spell_out = false; let result = input.try_parse(|input| { let ident = input.expect_ident().map_err(|_| ())?; match_ignore_ascii_case! { &*ident, "auto" => Ok(SpeakAs::Auto), "bullets" => Ok(SpeakAs::Bullets), "numbers" => Ok(SpeakAs::Numbers), "words" => Ok(SpeakAs::Words), "spell-out" => { is_spell_out = true; Err(()) }, _ => Err(()), } }); if is_spell_out { // spell-out is not supported, but don’t parse it as a . // See bug 1024178. return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?))) } } ================================================ FILE: style/counter_style/predefined.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ predefined! { "decimal", "decimal-leading-zero", "arabic-indic", "armenian", "upper-armenian", "lower-armenian", "bengali", "cambodian", "khmer", "cjk-decimal", "devanagari", "georgian", "gujarati", "gurmukhi", "hebrew", "kannada", "lao", "malayalam", "mongolian", "myanmar", "oriya", "persian", "lower-roman", "upper-roman", "tamil", "telugu", "thai", "tibetan", "lower-alpha", "lower-latin", "upper-alpha", "upper-latin", "cjk-earthly-branch", "cjk-heavenly-stem", "lower-greek", "hiragana", "hiragana-iroha", "katakana", "katakana-iroha", "disc", "circle", "square", "disclosure-open", "disclosure-closed", "japanese-informal", "japanese-formal", "korean-hangul-formal", "korean-hanja-informal", "korean-hanja-formal", "simp-chinese-informal", "simp-chinese-formal", "trad-chinese-informal", "trad-chinese-formal", "cjk-ideographic", "ethiopic-numeric", } ================================================ FILE: style/counter_style/update_predefined.py ================================================ #!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. */ from os.path import join, dirname import re from urllib.request import urlopen def main(filename): names = [ re.search('>([^>]+)(| VariableValue; struct EnvironmentVariable { name: Atom, evaluator: EnvironmentEvaluator, } macro_rules! make_variable { ($name:expr, $evaluator:expr) => {{ EnvironmentVariable { name: $name, evaluator: $evaluator, } }}; } fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels(device.safe_area_insets().top, url_data) } fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels(device.safe_area_insets().bottom, url_data) } fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels(device.safe_area_insets().left, url_data) } fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels(device.safe_area_insets().right, url_data) } #[cfg(feature = "gecko")] fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue { use crate::queries::values::PrefersColorScheme; let prefers_color_scheme = unsafe { crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme( device.document(), /* use_content = */ true, ) }; VariableValue::ident( match prefers_color_scheme { PrefersColorScheme::Light => "light", PrefersColorScheme::Dark => "dark", }, url_data, ) } #[cfg(feature = "servo")] fn get_content_preferred_color_scheme(_device: &Device, url_data: &UrlExtraData) -> VariableValue { // TODO: Add an implementation for Servo. VariableValue::ident("light", url_data) } fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels(device.scrollbar_inline_size().px(), url_data) } fn get_hairline(device: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::pixels( app_units::Au(device.app_units_per_device_pixel()).to_f32_px(), url_data, ) } static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top), make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom), make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left), make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right), ]; #[cfg(feature = "gecko")] macro_rules! lnf_int { ($id:ident) => { unsafe { crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt( crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32, ) } }; } #[cfg(feature = "servo")] macro_rules! lnf_int { ($id:ident) => { // TODO: Add an implementation for Servo. 0 }; } macro_rules! lnf_int_variable { ($atom:expr, $id:ident, $ctor:ident) => {{ fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue { VariableValue::$ctor(lnf_int!($id), url_data) } make_variable!($atom, __eval) }}; } fn eval_gtk_csd_titlebar_radius(device: &Device, url_data: &UrlExtraData) -> VariableValue { let int_pixels = lnf_int!(TitlebarRadius); let unzoomed_scale = device.device_pixel_ratio_ignoring_full_zoom().get() / device.device_pixel_ratio().get(); VariableValue::pixels(int_pixels as f32 * unzoomed_scale, url_data) } static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 9] = [ make_variable!( atom!("-moz-gtk-csd-titlebar-radius"), eval_gtk_csd_titlebar_radius ), lnf_int_variable!( atom!("-moz-gtk-csd-tooltip-radius"), TooltipRadius, int_pixels ), lnf_int_variable!( atom!("-moz-gtk-csd-close-button-position"), GTKCSDCloseButtonPosition, integer ), lnf_int_variable!( atom!("-moz-gtk-csd-minimize-button-position"), GTKCSDMinimizeButtonPosition, integer ), lnf_int_variable!( atom!("-moz-gtk-csd-maximize-button-position"), GTKCSDMaximizeButtonPosition, integer ), lnf_int_variable!( atom!("-moz-overlay-scrollbar-fade-duration"), ScrollbarFadeDuration, int_ms ), make_variable!( atom!("-moz-content-preferred-color-scheme"), get_content_preferred_color_scheme ), make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size), make_variable!(atom!("hairline"), get_hairline), ]; impl CssEnvironment { #[inline] fn get(&self, name: &Atom, device: &Device, url_data: &UrlExtraData) -> Option { if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { return Some((var.evaluator)(device, url_data)); } if !url_data.chrome_rules_enabled() { return None; } let var = CHROME_ENVIRONMENT_VARIABLES .iter() .find(|var| var.name == *name)?; Some((var.evaluator)(device, url_data)) } } /// A custom property name is just an `Atom`. /// /// Note that this does not include the `--` prefix pub type Name = Atom; /// Parse a custom property name. /// /// pub fn parse_name(s: &str) -> Result<&str, ()> { if s.starts_with("--") && s.len() > 2 { Ok(&s[2..]) } else { Err(()) } } /// A value for a custom property is just a set of tokens. /// /// We preserve the original CSS for serialization, and also the variable /// references to other custom property names. #[derive(Clone, Debug, MallocSizeOf, ToShmem)] pub struct VariableValue { /// The raw CSS string. pub css: String, /// The url data of the stylesheet where this value came from. pub url_data: UrlExtraData, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, /// var(), env(), attr() or non-custom property (e.g. through `em`) references. references: References, } trivial_to_computed_value!(VariableValue); /// Given a potentially registered variable value turn it into a computed custom property value. pub fn compute_variable_value( value: &Arc, registration: &PropertyDescriptors, computed_context: &computed::Context, ) -> Option { if registration.is_universal() { return Some(ComputedRegisteredValue::universal(Arc::clone(value))); } compute_value( &value.css, &value.url_data, registration, computed_context, AttrTaint::default(), ) .ok() } // For all purposes, we want values to be considered equal if their css text is equal. impl PartialEq for VariableValue { fn eq(&self, other: &Self) -> bool { self.css == other.css } } impl Eq for VariableValue {} impl ToCss for SpecifiedValue { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { dest.write_str(&self.css) } } impl ToTyped for SpecifiedValue { fn to_typed(&self, dest: &mut ThinVec) -> Result<(), ()> { let unparsed_value = reify_variable_value(self)?; dest.push(TypedValue::Unparsed(unparsed_value)); Ok(()) } } fn reify_variable_value(value: &VariableValue) -> Result { let mut reference_index = 0; reify_variable_value_range( &value.css, &value.references.refs, &mut reference_index, 0, value.css.len(), ) } /// Reify a slice of the CSS string into UnparsedSegment entries. /// /// References are stored in source order, with outer substitution functions /// inserted before references in their fallback. The shared `reference_index` /// relies on this ordering to recurse into fallbacks without reprocessing /// nested referecences. fn reify_variable_value_range( css: &str, references: &[SubstitutionFunctionReference], reference_index: &mut usize, start: usize, end: usize, ) -> Result { debug_assert!(start <= end); debug_assert!(end <= css.len()); let mut values = ThinVec::new(); let mut cur_pos = start; while *reference_index < references.len() { let reference = &references[*reference_index]; if reference.start >= end { break; } debug_assert!(reference.start >= cur_pos); debug_assert!(reference.start <= reference.end); debug_assert!(reference.end <= css.len()); if cur_pos < reference.start { values.push(UnparsedSegment::String(CssString::from( &css[cur_pos..reference.start], ))); } *reference_index += 1; if reference.substitution_kind != SubstitutionFunctionKind::Var { return Err(()); } let (fallback, has_fallback) = if let Some(fallback) = &reference.fallback { debug_assert!(fallback.start.get() <= reference.end - 1); ( reify_variable_value_range( css, references, reference_index, fallback.start.get(), reference.end - 1, // Skip the closing ')'. )?, true, ) } else { (ThinVec::new(), false) }; values.push(UnparsedSegment::VariableReference(VariableReferenceValue { variable: CssString::from(format!("--{}", reference.name)), fallback, has_fallback, })); cur_pos = reference.end; } if cur_pos < end { values.push(UnparsedSegment::String(CssString::from(&css[cur_pos..end]))); } Ok(values) } /// A pair of separate CustomPropertiesMaps, split between custom properties /// that have the inherit flag set and those with the flag unset. #[repr(C)] #[derive(Clone, Debug, Default, PartialEq)] pub struct ComputedCustomProperties { /// Map for custom properties with inherit flag set, including non-registered /// ones. pub inherited: CustomPropertiesMap, /// Map for custom properties with inherit flag unset. pub non_inherited: CustomPropertiesMap, } impl ComputedCustomProperties { /// Return whether the inherited and non_inherited maps are none. pub fn is_empty(&self) -> bool { self.inherited.is_empty() && self.non_inherited.is_empty() } /// Return the name and value of the property at specified index, if any. pub fn property_at(&self, index: usize) -> Option<(&Name, &Option)> { // Just expose the custom property items from custom_properties.inherited, followed // by custom property items from custom_properties.non_inherited. self.inherited .get_index(index) .or_else(|| self.non_inherited.get_index(index - self.inherited.len())) } /// Insert a custom property in the corresponding inherited/non_inherited /// map, depending on whether the inherit flag is set or unset. pub(crate) fn insert( &mut self, registration: &PropertyDescriptors, name: &Name, value: ComputedRegisteredValue, ) { self.map_mut(registration).insert(name, value) } /// Remove a custom property from the corresponding inherited/non_inherited /// map, depending on whether the inherit flag is set or unset. pub(crate) fn remove(&mut self, registration: &PropertyDescriptors, name: &Name) { self.map_mut(registration).remove(name); } /// Shrink the capacity of the inherited maps as much as possible. fn shrink_to_fit(&mut self) { self.inherited.shrink_to_fit(); self.non_inherited.shrink_to_fit(); } fn map_mut(&mut self, registration: &PropertyDescriptors) -> &mut CustomPropertiesMap { if registration.inherits() { &mut self.inherited } else { &mut self.non_inherited } } /// Returns the relevant custom property value given a registration. pub fn get( &self, registration: &PropertyDescriptors, name: &Name, ) -> Option<&ComputedRegisteredValue> { if registration.inherits() { self.inherited.get(name) } else { self.non_inherited.get(name) } } } /// Both specified and computed values are VariableValues, the difference is /// whether var() functions are expanded. pub type SpecifiedValue = VariableValue; /// Both specified and computed values are VariableValues, the difference is /// whether var() functions are expanded. pub type ComputedValue = VariableValue; /// Set of flags to non-custom references this custom property makes. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)] struct NonCustomReferences(u8); bitflags! { impl NonCustomReferences: u8 { /// At least one custom property depends on font-relative units. const FONT_UNITS = 1 << 0; /// At least one custom property depends on root element's font-relative units. const ROOT_FONT_UNITS = 1 << 1; /// At least one custom property depends on line height units. const LH_UNITS = 1 << 2; /// At least one custom property depends on root element's line height units. const ROOT_LH_UNITS = 1 << 3; /// All dependencies not depending on the root element. const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.0 | Self::LH_UNITS.0; /// All dependencies depending on the root element. const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.0 | Self::ROOT_LH_UNITS.0; } } impl NonCustomReferences { fn for_each(&self, mut f: F) where F: FnMut(SingleNonCustomReference), { for (_, r) in self.iter_names() { let single = match r { Self::FONT_UNITS => SingleNonCustomReference::FontUnits, Self::ROOT_FONT_UNITS => SingleNonCustomReference::RootFontUnits, Self::LH_UNITS => SingleNonCustomReference::LhUnits, Self::ROOT_LH_UNITS => SingleNonCustomReference::RootLhUnits, _ => unreachable!("Unexpected single bit value"), }; f(single); } } fn from_unit(value: &CowRcStr) -> Self { // For registered properties, any reference to font-relative dimensions // make it dependent on font-related properties. // TODO(dshin): When we unit algebra gets implemented and handled - // Is it valid to say that `calc(1em / 2em * 3px)` triggers this? if value.eq_ignore_ascii_case(FontRelativeLength::LH) { return Self::FONT_UNITS | Self::LH_UNITS; } if value.eq_ignore_ascii_case(FontRelativeLength::EM) || value.eq_ignore_ascii_case(FontRelativeLength::EX) || value.eq_ignore_ascii_case(FontRelativeLength::CAP) || value.eq_ignore_ascii_case(FontRelativeLength::CH) || value.eq_ignore_ascii_case(FontRelativeLength::IC) { return Self::FONT_UNITS; } if value.eq_ignore_ascii_case(FontRelativeLength::RLH) { return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS; } if value.eq_ignore_ascii_case(FontRelativeLength::REM) || value.eq_ignore_ascii_case(FontRelativeLength::REX) || value.eq_ignore_ascii_case(FontRelativeLength::RCH) || value.eq_ignore_ascii_case(FontRelativeLength::RCAP) || value.eq_ignore_ascii_case(FontRelativeLength::RIC) { return Self::ROOT_FONT_UNITS; } Self::empty() } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SingleNonCustomReference { FontUnits = 0, RootFontUnits, LhUnits, RootLhUnits, } struct NonCustomReferenceMap([Option; 4]); impl Default for NonCustomReferenceMap { fn default() -> Self { NonCustomReferenceMap(Default::default()) } } impl Index for NonCustomReferenceMap { type Output = Option; fn index(&self, reference: SingleNonCustomReference) -> &Self::Output { &self.0[reference as usize] } } impl IndexMut for NonCustomReferenceMap { fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output { &mut self.0[reference as usize] } } /// Whether to defer resolving custom properties referencing font relative units. #[derive(Clone, Copy, PartialEq, Eq)] #[allow(missing_docs)] pub enum DeferFontRelativeCustomPropertyResolution { Yes, No, } /// Substitution function source: var, env, attr. #[derive(Copy, Clone, Debug, MallocSizeOf, Hash, Eq, PartialEq, ToShmem, Parse)] pub enum SubstitutionFunctionKind { /// CSS variable / custom property Var, /// Environment variable Env, /// DOM attribute Attr, } /// A wrapper map that encapsulates both the custom properties and attributes /// for a given element. #[repr(C)] #[derive(Clone, Debug, Default, PartialEq)] pub struct ComputedSubstitutionFunctions { /// The applicable custom properties (includes inherited and non-inherited). pub custom_properties: ComputedCustomProperties, /// The applicable DOM attributes. pub attributes: OwnMap, } impl ComputedSubstitutionFunctions { /// Creates a substitution function map from optional custom properties /// and DOM attributes. #[inline(always)] pub fn new( custom_properties: Option, attributes: Option, ) -> Self { Self { custom_properties: custom_properties.unwrap_or_default(), attributes: attributes.unwrap_or_default(), } } #[inline(always)] fn insert_var( &mut self, registration: &PropertyDescriptors, name: &Name, value: ComputedRegisteredValue, ) { self.custom_properties.insert(registration, name, value); } #[inline(always)] fn insert_attr(&mut self, name: &Name, value: ComputedRegisteredValue) { self.attributes.insert(name.clone(), Some(value)); } #[inline(always)] fn remove_var(&mut self, registration: &PropertyDescriptors, name: &Name) { self.custom_properties.remove(registration, name); } #[inline(always)] fn remove_attr(&mut self, name: &Name) { self.attributes.insert(name.clone(), None); } #[inline(always)] fn get_var( &self, registration: &PropertyDescriptors, name: &Name, ) -> Option<&ComputedRegisteredValue> { self.custom_properties.get(registration, name) } #[inline(always)] fn get_attr(&self, name: &Name) -> Option<&ComputedRegisteredValue> { self.attributes.get(name).and_then(|p| p.as_ref()) } } #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, Parse)] enum AttributeType { None, RawString, Type(SyntaxDescriptor), Unit(AttrUnit), } #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] struct AttributeData { kind: AttributeType, namespace: ParsedNamespace, } /// For a CSS string, the range, counted in bytes, that is attr()-tainted. #[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem, ToComputedValue)] pub struct AttrTaintedRange { /// Start of the range, counted in bytes. Inclusive. start: usize, /// End of the range, counted in bytes. Exclusive. end: usize, } impl AttrTaintedRange { /// Creates a range within a CSS string that is tainted by attr(). #[inline(always)] pub fn new(start: usize, end: usize) -> Self { debug_assert!(start <= end); Self { start, end } } } /// In CSS Values and Units, values produced by `attr()` are considered attr()-tainted, as are /// functions that contain an attr()-tainted value. Using an attr()-tainted value as or in a /// makes a declaration invalid at computed-value time. /// https://drafts.csswg.org/css-values-5/#attr-security #[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] pub struct AttrTaint(SmallVec<[AttrTaintedRange; 1]>); impl AttrTaint { /// For a CSS string, determine whether any `` overlapping this `range` /// is disallowed due to attr()-tainting. #[inline(always)] pub fn should_disallow_urls_in_range(&self, range: &AttrTaintedRange) -> bool { self.0 .iter() .any(|r| r.start <= range.end && r.end >= range.start) } /// Returns true if the attr()-tainted range contains no elements. #[inline(always)] pub fn is_empty(&self) -> bool { self.0.is_empty() } #[inline(always)] fn new_fully_tainted(end: usize) -> Self { let mut taint = Self::default(); taint.push(0, end); taint } #[inline(always)] fn push(&mut self, start: usize, end: usize) { self.0.push(AttrTaintedRange::new(start, end)); } } #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] struct VariableFallback { // NOTE(emilio): We don't track fallback end, because we rely on the missing closing // parenthesis, if any, to be inserted, which means that we can rely on our end being // reference.end - 1. start: num::NonZeroUsize, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, } #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] struct SubstitutionFunctionReference { name: Name, start: usize, end: usize, fallback: Option, attribute_data: AttributeData, prev_token_type: TokenSerializationType, next_token_type: TokenSerializationType, substitution_kind: SubstitutionFunctionKind, } /// A struct holding information about the external references to that a custom /// property value may have. #[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] struct References { refs: Vec, non_custom_references: NonCustomReferences, any_env: bool, any_var: bool, any_attr: bool, } impl References { fn has_references(&self) -> bool { !self.refs.is_empty() } fn non_custom_references(&self, is_root_element: bool) -> NonCustomReferences { let mut mask = NonCustomReferences::NON_ROOT_DEPENDENCIES; if is_root_element { mask |= NonCustomReferences::ROOT_DEPENDENCIES } self.non_custom_references & mask } } impl VariableValue { fn empty(url_data: &UrlExtraData) -> Self { Self { css: String::new(), last_token_type: Default::default(), first_token_type: Default::default(), url_data: url_data.clone(), references: Default::default(), } } /// Create a new custom property without parsing if the CSS is known to be valid and contain no /// references. pub fn new( css: String, url_data: &UrlExtraData, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, ) -> Self { Self { css, url_data: url_data.clone(), first_token_type, last_token_type, references: References::default(), } } fn push<'i>( &mut self, css: &str, css_first_token_type: TokenSerializationType, css_last_token_type: TokenSerializationType, attr_taint: Option<&mut AttrTaint>, ) -> Result<(), ()> { /// Prevent values from getting terribly big since you can use custom /// properties exponentially. /// /// This number (2MB) is somewhat arbitrary, but silly enough that no /// reasonable page should hit it. We could limit by number of total /// substitutions, but that was very easy to work around in practice /// (just choose a larger initial value and boom). const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024; if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES { return Err(()); } // This happens e.g. between two subsequent var() functions: // `var(--a)var(--b)`. // // In that case, css_*_token_type is nonsensical. if css.is_empty() { return Ok(()); } self.first_token_type.set_if_nothing(css_first_token_type); // If self.first_token_type was nothing, // self.last_token_type is also nothing and this will be false: if self .last_token_type .needs_separator_when_before(css_first_token_type) { self.css.push_str("/**/") } let start = self.css.len(); self.css.push_str(css); let end = self.css.len(); if let Some(taint) = attr_taint { taint.push(start, end); } self.last_token_type = css_last_token_type; Ok(()) } /// Parse a custom property value. pub fn parse<'i, 't>( input: &mut Parser<'i, 't>, namespaces: Option<&FxHashMap>, url_data: &UrlExtraData, ) -> Result> { let mut references = References::default(); let mut missing_closing_characters = String::new(); let start_position = input.position(); let (first_token_type, last_token_type) = parse_declaration_value( input, start_position, namespaces, &mut references, &mut missing_closing_characters, )?; let mut css = input .slice_from(start_position) .trim_ascii_start() .to_owned(); if !missing_closing_characters.is_empty() { // Unescaped backslash at EOF in a quoted string is ignored. if css.ends_with("\\") && matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'') { css.pop(); } css.push_str(&missing_closing_characters); } css.truncate(css.trim_ascii_end().len()); css.shrink_to_fit(); references.refs.shrink_to_fit(); Ok(Self { css, url_data: url_data.clone(), first_token_type, last_token_type, references, }) } /// Returns whether this value is tainted by `attr()`. pub fn is_attr_tainted(&self) -> bool { self.references.any_attr } /// Create VariableValue from an int. fn integer(number: i32, url_data: &UrlExtraData) -> Self { Self::from_token( Token::Number { has_sign: false, value: number as f32, int_value: Some(number), }, url_data, ) } /// Create VariableValue from an int. fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self { Self::from_token(Token::Ident(ident.into()), url_data) } /// Create VariableValue from a float amount of CSS pixels. fn pixels(number: f32, url_data: &UrlExtraData) -> Self { // FIXME (https://github.com/servo/rust-cssparser/issues/266): // No way to get TokenSerializationType::Dimension without creating // Token object. Self::from_token( Token::Dimension { has_sign: false, value: number, int_value: None, unit: CowRcStr::from("px"), }, url_data, ) } /// Create VariableValue from an integer amount of milliseconds. fn int_ms(number: i32, url_data: &UrlExtraData) -> Self { Self::from_token( Token::Dimension { has_sign: false, value: number as f32, int_value: Some(number), unit: CowRcStr::from("ms"), }, url_data, ) } /// Create VariableValue from an integer amount of CSS pixels. fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self { Self::from_token( Token::Dimension { has_sign: false, value: number as f32, int_value: Some(number), unit: CowRcStr::from("px"), }, url_data, ) } fn from_token(token: Token, url_data: &UrlExtraData) -> Self { let token_type = token.serialization_type(); let mut css = token.to_css_string(); css.shrink_to_fit(); VariableValue { css, url_data: url_data.clone(), first_token_type: token_type, last_token_type: token_type, references: Default::default(), } } /// Returns the raw CSS text from this VariableValue pub fn css_text(&self) -> &str { &self.css } /// Returns whether this variable value has any reference to the environment or other /// variables. pub fn has_references(&self) -> bool { self.references.has_references() } } /// fn parse_declaration_value<'i, 't>( input: &mut Parser<'i, 't>, input_start: SourcePosition, namespaces: Option<&FxHashMap>, references: &mut References, missing_closing_characters: &mut String, ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { parse_declaration_value_block( input, input_start, namespaces, references, missing_closing_characters, ) }) } /// Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level. fn parse_declaration_value_block<'i, 't>( input: &mut Parser<'i, 't>, input_start: SourcePosition, namespaces: Option<&FxHashMap>, references: &mut References, missing_closing_characters: &mut String, ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { let mut is_first = true; let mut first_token_type = TokenSerializationType::Nothing; let mut last_token_type = TokenSerializationType::Nothing; let mut prev_reference_index: Option = None; loop { let token_start = input.position(); let Ok(token) = input.next_including_whitespace_and_comments() else { break; }; let prev_token_type = last_token_type; let serialization_type = token.serialization_type(); last_token_type = serialization_type; if is_first { first_token_type = last_token_type; is_first = false; } macro_rules! nested { ($closing:expr) => {{ let mut inner_end_position = None; let result = input.parse_nested_block(|input| { let result = parse_declaration_value_block( input, input_start, namespaces, references, missing_closing_characters, )?; inner_end_position = Some(input.position()); Ok(result) })?; if inner_end_position.unwrap() == input.position() { missing_closing_characters.push_str($closing); } result }}; } if let Some(index) = prev_reference_index.take() { references.refs[index].next_token_type = serialization_type; } match *token { Token::Comment(_) => { let token_slice = input.slice_from(token_start); if !token_slice.ends_with("*/") { missing_closing_characters.push_str(if token_slice.ends_with('*') { "/" } else { "*/" }) } }, Token::BadUrl(ref u) => { let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock(u.clone()); return Err(input.new_custom_error(e)); }, Token::BadString(ref s) => { let e = StyleParseErrorKind::BadStringInDeclarationValueBlock(s.clone()); return Err(input.new_custom_error(e)); }, Token::CloseParenthesis => { let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock; return Err(input.new_custom_error(e)); }, Token::CloseSquareBracket => { let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock; return Err(input.new_custom_error(e)); }, Token::CloseCurlyBracket => { let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock; return Err(input.new_custom_error(e)); }, Token::Function(ref name) => { let substitution_kind = match SubstitutionFunctionKind::from_ident(name).ok() { Some(SubstitutionFunctionKind::Attr) => { if static_prefs::pref!("layout.css.attr.enabled") { Some(SubstitutionFunctionKind::Attr) } else { None } }, kind => kind, }; if let Some(substitution_kind) = substitution_kind { let our_ref_index = references.refs.len(); let mut input_end_position = None; let fallback = input.parse_nested_block(|input| { let mut namespace = ParsedNamespace::Known(Namespace::default()); if substitution_kind == SubstitutionFunctionKind::Attr { if let Some(namespaces) = namespaces { if let Ok(ns) = input .try_parse(|input| ParsedNamespace::parse(namespaces, input)) { namespace = ns; } } } // TODO(emilio): For env() this should be per spec, but no other browser does // that, see https://github.com/w3c/csswg-drafts/issues/3262. let name = input.expect_ident()?; let name = Atom::from(if substitution_kind == SubstitutionFunctionKind::Var { match parse_name(name.as_ref()) { Ok(name) => name, Err(()) => { let name = name.clone(); return Err(input.new_custom_error( SelectorParseErrorKind::UnexpectedIdent(name), )); }, } } else { name.as_ref() }); let attribute_kind = if substitution_kind == SubstitutionFunctionKind::Attr { parse_attr_type(input) } else { AttributeType::None }; // We want the order of the references to match source order. So we need to reserve our slot // now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since // if this fails we discard the whole result anyways. let start = token_start.byte_index() - input_start.byte_index(); references.refs.push(SubstitutionFunctionReference { name, start, // To be fixed up after parsing fallback and auto-closing via our_ref_index. end: start, prev_token_type, // To be fixed up (if needed) on the next loop iteration via prev_reference_index. next_token_type: TokenSerializationType::Nothing, // To be fixed up after parsing fallback. fallback: None, attribute_data: AttributeData { kind: attribute_kind, namespace, }, substitution_kind: substitution_kind.clone(), }); let mut fallback = None; if input.try_parse(|input| input.expect_comma()).is_ok() { input.skip_whitespace(); let fallback_start = num::NonZeroUsize::new( input.position().byte_index() - input_start.byte_index(), ) .unwrap(); // NOTE(emilio): Intentionally using parse_declaration_value rather than // parse_declaration_value_block, since that's what parse_fallback used to do. let (first, last) = parse_declaration_value( input, input_start, namespaces, references, missing_closing_characters, )?; fallback = Some(VariableFallback { start: fallback_start, first_token_type: first, last_token_type: last, }); input_end_position = Some(input.position()); } else { let state = input.state(); // We still need to consume the rest of the potentially-unclosed // tokens, but make sure to not consume tokens that would otherwise be // invalid, by calling reset(). parse_declaration_value_block( input, input_start, namespaces, references, missing_closing_characters, )?; input_end_position = Some(input.position()); input.reset(&state); } Ok(fallback) })?; if input_end_position.unwrap() == input.position() { missing_closing_characters.push_str(")"); } prev_reference_index = Some(our_ref_index); let reference = &mut references.refs[our_ref_index]; reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len(); reference.fallback = fallback; match substitution_kind { SubstitutionFunctionKind::Var => references.any_var = true, SubstitutionFunctionKind::Env => references.any_env = true, SubstitutionFunctionKind::Attr => references.any_attr = true, }; } else { nested!(")"); } }, Token::ParenthesisBlock => { nested!(")"); }, Token::CurlyBracketBlock => { nested!("}"); }, Token::SquareBracketBlock => { nested!("]"); }, Token::QuotedString(_) => { let token_slice = input.slice_from(token_start); let quote = &token_slice[..1]; debug_assert!(matches!(quote, "\"" | "'")); if !(token_slice.ends_with(quote) && token_slice.len() > 1) { missing_closing_characters.push_str(quote) } }, Token::Ident(ref value) | Token::AtKeyword(ref value) | Token::Hash(ref value) | Token::IDHash(ref value) | Token::UnquotedUrl(ref value) | Token::Dimension { unit: ref value, .. } => { references .non_custom_references .insert(NonCustomReferences::from_unit(value)); let is_unquoted_url = matches!(token, Token::UnquotedUrl(_)); if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") { // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD // Check the value in case the final backslash was itself escaped. // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD. // (Unescaped U+FFFD would also work, but removing the backslash is annoying.) missing_closing_characters.push_str("�") } if is_unquoted_url && !input.slice_from(token_start).ends_with(")") { missing_closing_characters.push_str(")"); } }, _ => {}, }; } Ok((first_token_type, last_token_type)) } /// Parse = type( ) | raw-string | number | . /// https://drafts.csswg.org/css-values-5/#attr-notation fn parse_attr_type<'i, 't>(input: &mut Parser<'i, 't>) -> AttributeType { input .try_parse(|input| { Ok(match input.next()? { Token::Function(ref name) if name.eq_ignore_ascii_case("type") => { AttributeType::Type( input.parse_nested_block(SyntaxDescriptor::from_css_parser)?, ) }, Token::Ident(ref ident) => { if ident.eq_ignore_ascii_case("raw-string") { AttributeType::RawString } else { let unit = AttrUnit::from_ident(ident).map_err(|_| { input.new_custom_error(StyleParseErrorKind::UnspecifiedError) })?; AttributeType::Unit(unit) } }, Token::Delim('%') => AttributeType::Unit(AttrUnit::Percentage), _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }) }) .unwrap_or(AttributeType::None) } /// Attribute values may reference other substitution functions we may need to process. /// See step 6: https://drafts.csswg.org/css-values-5/#attr-substitution fn parse_attribute_value( name: &Atom, attribute_data: &AttributeData, url_data: &UrlExtraData, attribute_tracker: &mut AttributeTracker, ) -> Result { #[cfg(feature = "gecko")] let local_name = LocalName::cast(name); #[cfg(feature = "servo")] let local_name = &LocalName::from(name.as_ref()); let namespace = match attribute_data.namespace { ParsedNamespace::Known(ref ns) => ns, ParsedNamespace::Unknown => return Err(()), }; let attr = attribute_tracker.query(local_name, namespace).ok_or(())?; let mut input = ParserInput::new(&attr); let mut parser = Parser::new(&mut input); // TODO(Bug 2021110): Support namespaced attributes in chained references. let value = VariableValue::parse(&mut parser, None, &url_data).map_err(|_| ())?; Ok(ComputedRegisteredValue::universal(Arc::new(value))) } #[derive(Default)] struct SeenSubstitutionFunctions<'a> { var: PrecomputedHashSet<&'a Name>, attr: PrecomputedHashSet<&'a Name>, } /// A struct that takes care of encapsulating the cascade process for custom properties. pub struct CustomPropertiesBuilder<'a, 'b: 'a> { seen: SeenSubstitutionFunctions<'a>, may_have_cycles: bool, has_color_scheme: bool, substitution_functions: ComputedSubstitutionFunctions, reverted: PrecomputedHashMap<&'a Name, (CascadePriority, RevertKind)>, stylist: &'a Stylist, computed_context: &'a mut computed::Context<'b>, references_from_non_custom_properties: NonCustomReferenceMap>, } fn find_non_custom_references( registration: &PropertyDescriptors, value: &VariableValue, may_have_color_scheme: bool, is_root_element: bool, include_universal: bool, ) -> Option { let syntax = registration.syntax.as_ref()?; let dependent_types = syntax.dependent_types(); let may_reference_length = dependent_types.intersects(DependentDataTypes::LENGTH) || (include_universal && syntax.is_universal()); if may_reference_length { let value_dependencies = value.references.non_custom_references(is_root_element); if !value_dependencies.is_empty() { return Some(value_dependencies); } } if dependent_types.intersects(DependentDataTypes::COLOR) && may_have_color_scheme { // NOTE(emilio): We might want to add a NonCustomReferences::COLOR_SCHEME or something but // it's not really needed for correctness, so for now we use an Option for that to signal // that there might be a dependencies. return Some(NonCustomReferences::empty()); } None } impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { /// Create a new builder, inheriting from a given custom properties map. /// /// We expose this publicly mostly for @keyframe blocks. pub fn new_with_properties( stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>, ) -> Self { Self { seen: SeenSubstitutionFunctions::default(), reverted: Default::default(), may_have_cycles: false, has_color_scheme: false, substitution_functions: ComputedSubstitutionFunctions::new( Some(custom_properties), None, ), stylist, computed_context, references_from_non_custom_properties: NonCustomReferenceMap::default(), } } /// Create a new builder, inheriting from the right style given context. pub fn new(stylist: &'a Stylist, context: &'a mut computed::Context<'b>) -> Self { let is_root_element = context.is_root_element(); let inherited = context.inherited_custom_properties(); let initial_values = stylist.get_custom_property_initial_values(); let properties = ComputedCustomProperties { inherited: if is_root_element { debug_assert!(inherited.is_empty()); initial_values.inherited.clone() } else { inherited.inherited.clone() }, non_inherited: initial_values.non_inherited.clone(), }; // Reuse flags from computing registered custom properties initial values, such as // whether they depend on viewport units. context .style() .add_flags(stylist.get_custom_property_initial_values_flags()); Self::new_with_properties(stylist, properties, context) } /// Cascade a given custom property declaration. pub fn cascade( &mut self, declaration: &'a CustomDeclaration, priority: CascadePriority, attribute_tracker: &mut AttributeTracker, ) { let CustomDeclaration { ref name, ref value, } = *declaration; if let Some(&(reverted_priority, revert_kind)) = self.reverted.get(&name) { if !reverted_priority.allows_when_reverted(&priority, revert_kind) { return; } } if !(priority.flags() - self.computed_context.included_cascade_flags).is_empty() { return; } let was_already_present = !self.seen.var.insert(name); if was_already_present { return; } if !self.value_may_affect_style(name, value) { return; } let kind = SubstitutionFunctionKind::Var; let map = &mut self.substitution_functions; let registration = self.stylist.get_custom_property_registration(&name); match value { CustomDeclarationValue::Unparsed(unparsed_value) => { // At this point of the cascade we're not guaranteed to have seen the color-scheme // declaration, so need to assume the worst. We could track all system color // keyword tokens + the light-dark() function, but that seems non-trivial / // probably overkill. let may_have_color_scheme = true; // Non-custom dependency is really relevant for registered custom properties // that require computed value of such dependencies. let has_dependency = unparsed_value.references.any_var || unparsed_value.references.any_attr || find_non_custom_references( registration, unparsed_value, may_have_color_scheme, self.computed_context.is_root_element(), /* include_unregistered = */ false, ) .is_some(); // If the variable value has no references to other properties, perform // substitution here instead of forcing a full traversal in `substitute_all` // afterwards. if !has_dependency { return substitute_references_if_needed_and_apply( name, kind, unparsed_value, &mut self.substitution_functions, self.stylist, self.computed_context, attribute_tracker, ); } self.may_have_cycles = true; let value = ComputedRegisteredValue::universal(Arc::clone(unparsed_value)); map.insert_var(registration, name, value); }, CustomDeclarationValue::Parsed(parsed_value) => { let value = parsed_value.to_computed_value(&self.computed_context); map.insert_var(registration, name, value); }, CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword.revert_kind() { Some(revert_kind) => { self.seen.var.remove(name); self.reverted.insert(name, (priority, revert_kind)); }, None => match keyword { CSSWideKeyword::Initial => { // For non-inherited custom properties, 'initial' was handled in value_may_affect_style. debug_assert!(registration.inherits(), "Should've been handled earlier"); remove_and_insert_initial_value(name, registration, map); }, CSSWideKeyword::Inherit => { // For inherited custom properties, 'inherit' was handled in value_may_affect_style. debug_assert!(!registration.inherits(), "Should've been handled earlier"); self.computed_context .style() .add_flags(ComputedValueFlags::INHERITS_RESET_STYLE); if let Some(inherited_value) = self .computed_context .inherited_custom_properties() .non_inherited .get(name) { map.insert_var(registration, name, inherited_value.clone()); } }, // handled in value_may_affect_style or in the revert_kind branch above. CSSWideKeyword::Revert | CSSWideKeyword::RevertLayer | CSSWideKeyword::RevertRule | CSSWideKeyword::Unset => unreachable!(), }, }, } } /// Fast check to avoid calling maybe_note_non_custom_dependency in ~all cases. #[inline] pub fn might_have_non_custom_or_attr_dependency( id: LonghandId, decl: &PropertyDeclaration, ) -> bool { if id == LonghandId::ColorScheme { return true; } if let PropertyDeclaration::WithVariables(v) = decl { return matches!(id, LonghandId::LineHeight | LonghandId::FontSize) || v.value.variable_value.references.any_attr; } false } /// Note a non-custom property with variable reference that may in turn depend on that property. /// e.g. `font-size` depending on a custom property that may be a registered property using `em`. pub fn maybe_note_non_custom_dependency( &mut self, id: LonghandId, decl: &'a PropertyDeclaration, attribute_tracker: &mut AttributeTracker, ) { debug_assert!(Self::might_have_non_custom_or_attr_dependency(id, decl)); if id == LonghandId::ColorScheme { // If we might change the color-scheme, we need to defer computation of colors. self.has_color_scheme = true; return; } let PropertyDeclaration::WithVariables(v) = decl else { return; }; let value = &v.value.variable_value; let refs = &value.references; if !refs.any_var && !refs.any_attr { return; } // Attributes in non-custom properties may reference `var()` or `attr()` in their // values, which we need to track to support chained references and detect cycles. // Further processing occurs during `CustomPropertiesBuilder::build()`. if refs.any_attr { self.update_attributes_map(value, attribute_tracker); if !refs.any_var { return; } } // With unit algebra in `calc()`, references aren't limited to `font-size`. // For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`, // or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);` let references = match id { LonghandId::FontSize => { if self.computed_context.is_root_element() { NonCustomReferences::ROOT_FONT_UNITS } else { NonCustomReferences::FONT_UNITS } }, LonghandId::LineHeight => { if self.computed_context.is_root_element() { NonCustomReferences::ROOT_LH_UNITS | NonCustomReferences::ROOT_FONT_UNITS } else { NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS } }, _ => return, }; let variables: Vec = refs .refs .iter() .filter_map(|reference| { if reference.substitution_kind != SubstitutionFunctionKind::Var { return None; } let registration = self .stylist .get_custom_property_registration(&reference.name); if !registration .syntax .as_ref()? .dependent_types() .intersects(DependentDataTypes::LENGTH) { return None; } Some(reference.name.clone()) }) .collect(); references.for_each(|idx| { let entry = &mut self.references_from_non_custom_properties[idx]; let was_none = entry.is_none(); let v = entry.get_or_insert_with(|| variables.clone()); if was_none { return; } v.extend(variables.iter().cloned()); }); } fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { let registration = self.stylist.get_custom_property_registration(&name); match *value { CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { // For inherited custom properties, explicit 'inherit' means we // can just use any existing value in the inherited // CustomPropertiesMap. if registration.inherits() { return false; } }, CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) => { // For non-inherited custom properties, explicit 'initial' means // we can just use any initial value in the registration. if !registration.inherits() { return false; } }, CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) => { // Explicit 'unset' means we can either just use any existing // value in the inherited CustomPropertiesMap or the initial // value in the registration. return false; }, _ => {}, } let existing_value = self.substitution_functions.get_var(registration, &name); let existing_value = match existing_value { None => { if matches!( value, CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) ) { debug_assert!(registration.inherits(), "Should've been handled earlier"); // The initial value of a custom property without a // guaranteed-invalid initial value is the same as it // not existing in the map. if registration.initial_value.is_none() { return false; } } return true; }, Some(v) => v, }; let computed_value = match value { CustomDeclarationValue::Unparsed(value) => { // Don't bother overwriting an existing value with the same // specified value. if let Some(existing_value) = existing_value.as_universal() { return existing_value != value; } if !registration.is_universal() { compute_value( &value.css, &value.url_data, registration, self.computed_context, AttrTaint::default(), ) .ok() } else { None } }, CustomDeclarationValue::Parsed(value) => { Some(value.to_computed_value(&self.computed_context)) }, CustomDeclarationValue::CSSWideKeyword(kw) => { match kw { CSSWideKeyword::Inherit => { debug_assert!(!registration.inherits(), "Should've been handled earlier"); // existing_value is the registered initial value. // Don't bother adding it to self.custom_properties.non_inherited // if the key is also absent from self.inherited.non_inherited. if self .computed_context .inherited_custom_properties() .non_inherited .get(name) .is_none() { return false; } }, CSSWideKeyword::Initial => { debug_assert!(registration.inherits(), "Should've been handled earlier"); // Don't bother overwriting an existing value with the initial value specified in // the registration. if let Some(initial_value) = self .stylist .get_custom_property_initial_values() .get(registration, name) { return existing_value != initial_value; } }, CSSWideKeyword::Unset => { debug_assert!(false, "Should've been handled earlier"); }, CSSWideKeyword::Revert | CSSWideKeyword::RevertLayer | CSSWideKeyword::RevertRule => {}, } None }, }; if let Some(value) = computed_value { return existing_value.v != value.v; } true } /// For a given unparsed variable, update the attributes map with its attr references. pub fn update_attributes_map( &mut self, value: &'a VariableValue, attribute_tracker: &mut AttributeTracker, ) { let refs = &value.references; if !refs.any_attr { return; } self.may_have_cycles = true; for next in &refs.refs { // Skip non-attrs and attributes we've already processed. if next.substitution_kind != SubstitutionFunctionKind::Attr || !self.seen.attr.insert(&next.name) { continue; } if let Ok(v) = parse_attribute_value( &next.name, &next.attribute_data, &value.url_data, attribute_tracker, ) { self.substitution_functions.insert_attr(&next.name, v); } } } /// Computes the map of applicable custom properties, as well as /// longhand properties that are now considered invalid-at-compute time. /// The result is saved into the computed context. /// /// If there was any specified property or non-inherited custom property /// with an initial value, we've created a new map and now we /// need to remove any potential cycles (And marking non-custom /// properties), and wrap it in an arc. /// /// Some registered custom properties may require font-related properties /// be resolved to resolve. If these properties are not resolved at this time, /// `defer` should be set to `Yes`, which will leave such custom properties, /// and other properties referencing them, untouched. These properties are /// returned separately, to be resolved by `build_deferred` to fully resolve /// all custom properties after all necessary non-custom properties are resolved. pub fn build( mut self, defer: DeferFontRelativeCustomPropertyResolution, attribute_tracker: &mut AttributeTracker, ) -> Option { let mut deferred_substitution_functions = None; if self.may_have_cycles { if defer == DeferFontRelativeCustomPropertyResolution::Yes { deferred_substitution_functions = Some(AllSubstitutionFunctions::default()); } let mut invalid_non_custom_properties = LonghandIdSet::default(); substitute_all( &mut self.substitution_functions, deferred_substitution_functions.as_mut(), &mut invalid_non_custom_properties, self.has_color_scheme, &self.seen, &self.references_from_non_custom_properties, self.stylist, self.computed_context, attribute_tracker, ); self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties; } self.substitution_functions .custom_properties .shrink_to_fit(); // Some pages apply a lot of redundant custom properties, see e.g. // bug 1758974 comment 5. Try to detect the case where the values // haven't really changed, and save some memory by reusing the inherited // map in that case. let initial_values = self.stylist.get_custom_property_initial_values(); let custom_properties = self.substitution_functions.custom_properties; self.computed_context .builder .substitution_functions .custom_properties = ComputedCustomProperties { inherited: if self .computed_context .inherited_custom_properties() .inherited == custom_properties.inherited { self.computed_context .inherited_custom_properties() .inherited .clone() } else { custom_properties.inherited }, non_inherited: if initial_values.non_inherited == custom_properties.non_inherited { initial_values.non_inherited.clone() } else { custom_properties.non_inherited }, }; self.computed_context .builder .substitution_functions .attributes = self.substitution_functions.attributes; deferred_substitution_functions } /// Fully resolve all deferred custom properties and attributes, assuming that the /// incoming context has necessary properties resolved. pub fn build_deferred( deferred: AllSubstitutionFunctions, stylist: &Stylist, computed_context: &mut computed::Context, attribute_tracker: &mut AttributeTracker, ) { if deferred.is_empty() { return; } let mut map = std::mem::take(&mut computed_context.builder.substitution_functions); // Since `CustomPropertiesMap` preserves insertion order, we shouldn't have to worry about // resolving in a wrong order. for (name, kind, v) in deferred.iter() { let Some(v) = v.as_universal() else { unreachable!("Computing should have been deferred!") }; substitute_references_if_needed_and_apply( name, kind, v, &mut map, stylist, computed_context, attribute_tracker, ); } computed_context.builder.substitution_functions = map; } } /// Resolve all custom properties to either substituted, invalid, or unset /// (meaning we should use the inherited value). /// /// It does cycle dependencies removal at the same time as substitution. fn substitute_all( substitution_function_map: &mut ComputedSubstitutionFunctions, mut deferred_substituted_functions_map: Option<&mut AllSubstitutionFunctions>, invalid_non_custom_properties: &mut LonghandIdSet, has_color_scheme: bool, seen: &SeenSubstitutionFunctions, references_from_non_custom_properties: &NonCustomReferenceMap>, stylist: &Stylist, computed_context: &computed::Context, attr_tracker: &mut AttributeTracker, ) { // The cycle dependencies removal in this function is a variant // of Tarjan's algorithm. It is mostly based on the pseudo-code // listed in // https://en.wikipedia.org/w/index.php? // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495 #[derive(Clone, Eq, PartialEq, Debug)] enum VarType { Attr(Name), Custom(Name), NonCustom(SingleNonCustomReference), } /// Struct recording necessary information for each variable. #[derive(Debug)] struct VarInfo { /// The name of the variable. It will be taken to save addref /// when the corresponding variable is popped from the stack. /// This also serves as a mark for whether the variable is /// currently in the stack below. var: Option, /// If the variable is in a dependency cycle, lowlink represents /// a smaller index which corresponds to a variable in the same /// strong connected component, which is known to be accessible /// from this variable. It is not necessarily the root, though. lowlink: usize, } /// Context struct for traversing the variable graph, so that we can /// avoid referencing all the fields multiple times. struct Context<'a, 'b: 'a> { /// Number of variables visited. This is used as the order index /// when we visit a new unresolved variable. count: usize, /// The map from custom property name to its order index. index_map: PrecomputedHashMap, /// Mapping from a non-custom dependency to its order index. non_custom_index_map: NonCustomReferenceMap, /// Information of each variable indexed by the order index. var_info: SmallVec<[VarInfo; 5]>, /// The stack of order index of visited variables. It contains /// all unfinished strong connected components. stack: SmallVec<[usize; 5]>, /// References to non-custom properties in this strongly connected component. non_custom_references: NonCustomReferences, /// Whether the builder has seen a non-custom color-scheme reference. has_color_scheme: bool, /// Whether this strongly connected component contains any custom properties involving /// value computation. contains_computed_custom_property: bool, map: &'a mut ComputedSubstitutionFunctions, /// The stylist is used to get registered properties, and to resolve the environment to /// substitute `env()` variables. stylist: &'a Stylist, /// The computed context is used to get inherited custom /// properties and compute registered custom properties. computed_context: &'a computed::Context<'b>, /// Longhand IDs that became invalid due to dependency cycle(s). invalid_non_custom_properties: &'a mut LonghandIdSet, /// Substitution functions that cannot yet be substituted. We store both custom /// properties (inherited and non-inherited) and attributes in the same map, since /// we need to make sure we iterate through them in the right order. deferred_substitution_functions: Option<&'a mut AllSubstitutionFunctions>, } /// This function combines the traversal for cycle removal and value /// substitution. It returns either a signal None if this variable /// has been fully resolved (to either having no reference or being /// marked invalid), or the order index for the given name. /// /// When it returns, the variable corresponds to the name would be /// in one of the following states: /// * It is still in context.stack, which means it is part of an /// potentially incomplete dependency circle. /// * It has been removed from the map. It can be either that the /// substitution failed, or it is inside a dependency circle. /// When this function removes a variable from the map because /// of dependency circle, it would put all variables in the same /// strong connected component to the set together. /// * It doesn't have any reference, because either this variable /// doesn't have reference at all in specified value, or it has /// been completely resolved. /// * There is no such variable at all. fn traverse<'a, 'b>( var: VarType, non_custom_references: &NonCustomReferenceMap>, context: &mut Context<'a, 'b>, attribute_tracker: &mut AttributeTracker, ) -> Option { let kind = if matches!(var, VarType::Custom(_)) { SubstitutionFunctionKind::Var } else { SubstitutionFunctionKind::Attr }; // Some shortcut checks. let value = match var { VarType::Custom(ref name) | VarType::Attr(ref name) => { let registration; let value; match kind { SubstitutionFunctionKind::Var => { registration = context.stylist.get_custom_property_registration(name); value = context.map.get_var(registration, name)?.as_universal()?; }, SubstitutionFunctionKind::Attr => { // FIXME(bug1997338): registration does not make much sense for attrs. // Rework find_non_custom_references to take Descriptor instead? registration = PropertyDescriptors::unregistered(); value = context.map.get_attr(name)?.as_universal()?; }, _ => unreachable!("Substitution kind must be var or attr for VarType::Custom."), } let is_var = kind == SubstitutionFunctionKind::Var; let is_attr = kind == SubstitutionFunctionKind::Attr; let is_root = context.computed_context.is_root_element(); // We need to keep track of potential non-custom-references even on unregistered // properties for cycle-detection purposes. let non_custom_refs = find_non_custom_references( registration, value, context.has_color_scheme, is_root, /* include_unregistered = */ true, ); context.non_custom_references |= non_custom_refs.unwrap_or_default(); let has_dependency = value.references.any_var || value.references.any_attr || non_custom_refs.is_some(); // Nothing to resolve. if !has_dependency { debug_assert!(!value.references.any_env, "Should've been handled earlier"); if is_attr || !registration.is_universal() { // We might still need to compute the value if this is not an universal // registration if we thought this had a dependency before but turned out // not to be (due to has_color_scheme, for example). Note that if this was // already computed we would've bailed out in the as_universal() check. if is_var { debug_assert!( registration .syntax .as_ref() .unwrap() .dependent_types() .intersects(DependentDataTypes::COLOR), "How did an unresolved value get here otherwise?", ); } let value = value.clone(); substitute_references_if_needed_and_apply( name, kind, &value, &mut context.map, context.stylist, context.computed_context, attribute_tracker, ); } return None; } // Has this variable been visited? // FIXME(bug1997338): a name conflict between between y and --y is possible // because they refer to the same atom. E.g. `attr(y type(*))` where // `y="var(--y)"` and `--y: var(--baz)`. match context.index_map.entry(name.clone()) { Entry::Occupied(entry) => { return Some(*entry.get()); }, Entry::Vacant(entry) => { entry.insert(context.count); }, } context.contains_computed_custom_property |= is_var && !registration.is_universal(); // Hold a strong reference to the value so that we don't // need to keep reference to context.map. Some(value.clone()) }, VarType::NonCustom(ref non_custom) => { let entry = &mut context.non_custom_index_map[*non_custom]; if let Some(v) = entry { return Some(*v); } *entry = Some(context.count); None }, }; // Add new entry to the information table. let index = context.count; context.count += 1; debug_assert_eq!(index, context.var_info.len()); context.var_info.push(VarInfo { var: Some(var.clone()), lowlink: index, }); context.stack.push(index); let mut self_ref = false; let mut lowlink = index; let visit_link = |var: VarType, context: &mut Context, lowlink: &mut usize, self_ref: &mut bool, attr_tracker: &mut AttributeTracker| { let next_index = match traverse(var, non_custom_references, context, attr_tracker) { Some(index) => index, // There is nothing to do if the next variable has been // fully resolved at this point. None => { return; }, }; let next_info = &context.var_info[next_index]; if next_index > index { // The next variable has a larger index than us, so it // must be inserted in the recursive call above. We want // to get its lowlink. *lowlink = cmp::min(*lowlink, next_info.lowlink); } else if next_index == index { *self_ref = true; } else if next_info.var.is_some() { // The next variable has a smaller order index and it is // in the stack, so we are at the same component. *lowlink = cmp::min(*lowlink, next_index); } }; if let Some(ref v) = value.as_ref() { debug_assert!( matches!(var, VarType::Custom(_) | VarType::Attr(_)), "Non-custom property has references?" ); // Visit other custom properties... // FIXME: Maybe avoid visiting the same var twice if not needed? for next in &v.references.refs { if next.substitution_kind == SubstitutionFunctionKind::Env { continue; } let next_var = if next.substitution_kind == SubstitutionFunctionKind::Attr { if context.map.get_attr(&next.name).is_none() { let Ok(val) = parse_attribute_value( &next.name, &next.attribute_data, &v.url_data, attribute_tracker, ) else { continue; }; context.map.insert_attr(&next.name, val); } VarType::Attr(next.name.clone()) } else { VarType::Custom(next.name.clone()) }; visit_link( next_var, context, &mut lowlink, &mut self_ref, attribute_tracker, ); } // ... Then non-custom properties. v.references.non_custom_references.for_each(|r| { visit_link( VarType::NonCustom(r), context, &mut lowlink, &mut self_ref, attribute_tracker, ); }); } else if let VarType::NonCustom(non_custom) = var { let entry = &non_custom_references[non_custom]; if let Some(deps) = entry.as_ref() { for d in deps { // Visit any reference from this non-custom property to custom properties. // TODO(bug1997338): non-custom properties can reference attrs. visit_link( VarType::Custom(d.clone()), context, &mut lowlink, &mut self_ref, attribute_tracker, ); } } } context.var_info[index].lowlink = lowlink; if lowlink != index { // This variable is in a loop, but it is not the root of // this strong connected component. We simply return for // now, and the root would remove it from the map. // // This cannot be removed from the map here, because // otherwise the shortcut check at the beginning of this // function would return the wrong value. return Some(index); } // This is the root of a strong-connected component. let mut in_loop = self_ref; let name; let handle_variable_in_loop = |name: &Name, context: &mut Context<'a, 'b>, kind: SubstitutionFunctionKind| { if context.contains_computed_custom_property { // These non-custom properties can't become invalid-at-compute-time from // cyclic dependencies purely consisting of non-registered properties. if context.non_custom_references.intersects( NonCustomReferences::FONT_UNITS | NonCustomReferences::ROOT_FONT_UNITS, ) { context .invalid_non_custom_properties .insert(LonghandId::FontSize); } if context.non_custom_references.intersects( NonCustomReferences::LH_UNITS | NonCustomReferences::ROOT_LH_UNITS, ) { context .invalid_non_custom_properties .insert(LonghandId::LineHeight); } } // This variable is in loop. Resolve to invalid. handle_invalid_at_computed_value_time( name, kind, &mut context.map, context.computed_context, ); }; loop { let var_index = context .stack .pop() .expect("The current variable should still be in stack"); let var_info = &mut context.var_info[var_index]; // We should never visit the variable again, so it's safe // to take the name away, so that we don't do additional // reference count. let var_name = var_info .var .take() .expect("Variable should not be poped from stack twice"); if var_index == index { name = match var_name { VarType::Custom(name) | VarType::Attr(name) => name, // At the root of this component, and it's a non-custom // reference - we have nothing to substitute, so // it's effectively resolved. VarType::NonCustom(..) => return None, }; break; } if let VarType::Custom(name) | VarType::Attr(name) = var_name { // Anything here is in a loop which can traverse to the // variable we are handling, so it's invalid at // computed-value time. handle_variable_in_loop(&name, context, kind); } in_loop = true; } // We've gotten to the root of this strongly connected component, so clear // whether or not it involved non-custom references. // It's fine to track it like this, because non-custom properties currently // being tracked can only participate in any loop only once. if in_loop { handle_variable_in_loop(&name, context, kind); context.non_custom_references = NonCustomReferences::default(); return None; } if let Some(ref v) = value { let registration = context.stylist.get_custom_property_registration(&name); let mut defer = false; if let Some(ref mut deferred) = context.deferred_substitution_functions { // We need to defer this property if it has a non-custom property dependency, or // any variable that it references is already deferred. defer = find_non_custom_references( registration, v, context.has_color_scheme, context.computed_context.is_root_element(), /* include_unregistered = */ false, ) .is_some() || v.references.refs.iter().any(|reference| { (reference.substitution_kind == SubstitutionFunctionKind::Var && deferred .get(&reference.name, SubstitutionFunctionKind::Var) .is_some()) || reference.substitution_kind == SubstitutionFunctionKind::Attr }); if defer { let value = ComputedRegisteredValue::universal(Arc::clone(v)); deferred.insert(&name, kind, value); if kind == SubstitutionFunctionKind::Var { context.map.remove_var(registration, &name); } else { context.map.remove_attr(&name); } } } // If there are no var or attr references we should already be computed and substituted by now. if !defer && (v.references.any_var || v.references.any_attr) { substitute_references_if_needed_and_apply( &name, kind, v, &mut context.map, context.stylist, context.computed_context, attribute_tracker, ); } } context.non_custom_references = NonCustomReferences::default(); // All resolved, so return the signal value. None } let mut run = |make_var: fn(Name) -> VarType, seen: &PrecomputedHashSet<&Name>| { for name in seen { let mut context = Context { count: 0, index_map: PrecomputedHashMap::default(), non_custom_index_map: NonCustomReferenceMap::default(), stack: SmallVec::new(), var_info: SmallVec::new(), map: substitution_function_map, non_custom_references: NonCustomReferences::default(), has_color_scheme, stylist, computed_context, invalid_non_custom_properties, deferred_substitution_functions: deferred_substituted_functions_map.as_deref_mut(), contains_computed_custom_property: false, }; traverse( make_var((*name).clone()), references_from_non_custom_properties, &mut context, attr_tracker, ); } }; // Note that `seen` doesn't contain names inherited from our parent, but // those can't have variable references (since we inherit the computed // variables) so we don't want to spend cycles traversing them anyway. run(VarType::Custom, &seen.var); // Traverse potentially untraversed chained references from `attr(type())` // in non-custom properties. run(VarType::Attr, &seen.attr); } // See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time fn handle_invalid_at_computed_value_time( name: &Name, kind: SubstitutionFunctionKind, substitution_functions: &mut ComputedSubstitutionFunctions, computed_context: &computed::Context, ) { if kind == SubstitutionFunctionKind::Attr { // Early return: `attr()` is always treated as unregistered. substitution_functions.remove_attr(name); return; } let stylist = computed_context.style().stylist.unwrap(); let registration = stylist.get_custom_property_registration(&name); if !registration.is_universal() { // For the root element, inherited maps are empty. We should just // use the initial value if any, rather than removing the name. if registration.inherits() && !computed_context.is_root_element() { let inherited = computed_context.inherited_custom_properties(); if let Some(value) = inherited.get(registration, name) { substitution_functions.insert_var(registration, name, value.clone()); return; } } else if let Some(ref initial_value) = registration.initial_value { if let Ok(initial_value) = compute_value( &initial_value.css, &initial_value.url_data, registration, computed_context, AttrTaint::default(), ) { substitution_functions.insert_var(registration, name, initial_value); return; } } } substitution_functions.remove_var(registration, name); } /// Replace `var()`, `env()`, and `attr()` functions in a pre-existing variable value. fn substitute_references_if_needed_and_apply( name: &Name, kind: SubstitutionFunctionKind, value: &Arc, substitution_functions: &mut ComputedSubstitutionFunctions, stylist: &Stylist, computed_context: &computed::Context, attribute_tracker: &mut AttributeTracker, ) { debug_assert_ne!(kind, SubstitutionFunctionKind::Env); let is_var = kind == SubstitutionFunctionKind::Var; let registration = stylist.get_custom_property_registration(&name); if is_var && !value.has_references() && registration.is_universal() { // Trivial path: no references and no need to compute the value, just apply it directly. let computed_value = ComputedRegisteredValue::universal(Arc::clone(value)); substitution_functions.insert_var(registration, name, computed_value); return; } let inherited = computed_context.inherited_custom_properties(); let url_data = &value.url_data; let substitution = match substitute_internal( value, substitution_functions, stylist, computed_context, attribute_tracker, None, ) { Ok(v) => v, Err(..) => { handle_invalid_at_computed_value_time( name, kind, substitution_functions, computed_context, ); return; }, }; // TODO(bug1997338): rework with attr. // If variable fallback results in a wide keyword, deal with it now. { let css = &substitution.css; let css_wide_kw = { let mut input = ParserInput::new(&css); let mut input = Parser::new(&mut input); input.try_parse(CSSWideKeyword::parse) }; if let Ok(kw) = css_wide_kw { // TODO: It's unclear what this should do for revert / revert-layer, see // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset // seems fine? match ( kw, registration.inherits(), computed_context.is_root_element(), ) { (CSSWideKeyword::Initial, _, _) | (CSSWideKeyword::Revert, false, _) | (CSSWideKeyword::RevertLayer, false, _) | (CSSWideKeyword::RevertRule, false, _) | (CSSWideKeyword::Unset, false, _) | (CSSWideKeyword::Revert, true, true) | (CSSWideKeyword::RevertLayer, true, true) | (CSSWideKeyword::RevertRule, true, true) | (CSSWideKeyword::Unset, true, true) | (CSSWideKeyword::Inherit, _, true) => { remove_and_insert_initial_value(name, registration, substitution_functions); }, (CSSWideKeyword::Revert, true, false) | (CSSWideKeyword::RevertLayer, true, false) | (CSSWideKeyword::RevertRule, true, false) | (CSSWideKeyword::Inherit, _, false) | (CSSWideKeyword::Unset, true, false) => { match inherited.get(registration, name) { Some(value) => { substitution_functions.insert_var(registration, name, value.clone()); }, None => { substitution_functions.remove_var(registration, name); }, }; }, } return; } } match kind { SubstitutionFunctionKind::Var => { let value = match substitution.into_value(url_data, registration, computed_context) { Ok(v) => v, Err(()) => { handle_invalid_at_computed_value_time( name, kind, substitution_functions, computed_context, ); return; }, }; substitution_functions.insert_var(registration, name, value); }, SubstitutionFunctionKind::Attr => { let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new( substitution.css.into_owned(), url_data, substitution.first_token_type, substitution.last_token_type, ))); value.attr_tainted |= substitution.attr_tainted; substitution_functions.insert_attr(name, value); }, SubstitutionFunctionKind::Env => unreachable!("Kind cannot be env."), } } #[derive(Default, Debug)] struct Substitution<'a> { css: Cow<'a, str>, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, attr_tainted: bool, } impl<'a> Substitution<'a> { fn from_value(v: VariableValue, attr_tainted: bool) -> Self { Substitution { css: v.css.into(), first_token_type: v.first_token_type, last_token_type: v.last_token_type, attr_tainted, } } fn into_value( self, url_data: &UrlExtraData, registration: &PropertyDescriptors, computed_context: &computed::Context, ) -> Result { if registration.is_universal() { let mut value = ComputedRegisteredValue::universal(Arc::new(VariableValue::new( self.css.into_owned(), url_data, self.first_token_type, self.last_token_type, ))); value.attr_tainted |= self.attr_tainted; return Ok(value); } let taint = if self.attr_tainted { // Per spec: substitution value of an arbitrary substitution function is // attr()-tainted as a whole if any attr()-tainted values were involved // in creating that substitution value. // https://drafts.csswg.org/css-values-5/#attr-security AttrTaint::new_fully_tainted(self.css.len()) } else { AttrTaint::default() }; let mut v = compute_value(&self.css, url_data, registration, computed_context, taint)?; v.attr_tainted |= self.attr_tainted; Ok(v) } fn new( css: Cow<'a, str>, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, attr_tainted: bool, ) -> Self { Self { css, first_token_type, last_token_type, attr_tainted, } } } /// Result of var(), env(), and attr() substitution. #[derive(Debug)] pub struct SubstitutionResult<'a> { /// The resolved CSS string after substitution. pub css: Cow<'a, str>, /// Regions in the `css` string that are attr()-tainted, if any. pub attr_taint: AttrTaint, } fn compute_value( css: &str, url_data: &UrlExtraData, registration: &PropertyDescriptors, computed_context: &computed::Context, attr_taint: AttrTaint, ) -> Result { debug_assert!(!registration.is_universal()); let mut input = ParserInput::new(&css); let mut input = Parser::new(&mut input); SpecifiedRegisteredValue::compute( &mut input, registration, None, url_data, computed_context, AllowComputationallyDependent::Yes, attr_taint, ) } /// Removes the named registered custom property and inserts its uncomputed initial value. fn remove_and_insert_initial_value( name: &Name, registration: &PropertyDescriptors, substitution_functions: &mut ComputedSubstitutionFunctions, ) { substitution_functions.remove_var(registration, name); if let Some(ref initial_value) = registration.initial_value { let value = ComputedRegisteredValue::universal(Arc::clone(initial_value)); substitution_functions.insert_var(registration, name, value); } } fn do_substitute_chunk<'a>( css: &'a str, start: usize, end: usize, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, url_data: &UrlExtraData, substitution_functions: &'a ComputedSubstitutionFunctions, stylist: &Stylist, computed_context: &computed::Context, references: &mut std::iter::Peekable>, attribute_tracker: &mut AttributeTracker, mut attr_taint: Option<&mut AttrTaint>, ) -> Result, ()> { if start == end { // Empty string. Easy. return Ok(Substitution::default()); } // Easy case: no references involved. if references .peek() .map_or(true, |reference| reference.end > end) { let result = &css[start..end]; return Ok(Substitution::new( Cow::Borrowed(result), first_token_type, last_token_type, Default::default(), )); } let mut substituted = ComputedValue::empty(url_data); let mut next_token_type = first_token_type; let mut cur_pos = start; let mut attr_tainted = false; while let Some(reference) = references.next_if(|reference| reference.end <= end) { if reference.start != cur_pos { substituted.push( &css[cur_pos..reference.start], next_token_type, reference.prev_token_type, /* attr_taint */ None, )?; } let substitution = substitute_one_reference( css, url_data, substitution_functions, reference, stylist, computed_context, references, attribute_tracker, )?; // Optimize the property: var(--...) case to avoid allocating at all. if reference.start == start && reference.end == end { if let Some(taint) = attr_taint.filter(|_| substitution.attr_tainted) { taint.push(start, end); } return Ok(substitution); } substituted.push( &substitution.css, substitution.first_token_type, substitution.last_token_type, attr_taint .as_deref_mut() .filter(|_| substitution.attr_tainted), )?; attr_tainted |= substitution.attr_tainted; next_token_type = reference.next_token_type; cur_pos = reference.end; } // Push the rest of the value if needed. if cur_pos != end { substituted.push( &css[cur_pos..end], next_token_type, last_token_type, /* attr_taint */ None, )?; } Ok(Substitution::from_value(substituted, attr_tainted)) } fn quoted_css_string(src: &str) -> String { let mut dest = String::with_capacity(src.len() + 2); cssparser::serialize_string(src, &mut dest).unwrap(); dest } fn substitute_one_reference<'a>( css: &'a str, url_data: &UrlExtraData, substitution_functions: &'a ComputedSubstitutionFunctions, reference: &SubstitutionFunctionReference, stylist: &Stylist, computed_context: &computed::Context, references: &mut std::iter::Peekable>, attribute_tracker: &mut AttributeTracker, ) -> Result, ()> { let simple_attr_subst = |s: &str| { Some(Substitution::new( Cow::Owned(quoted_css_string(s)), TokenSerializationType::Nothing, TokenSerializationType::Nothing, /* attr_tainted */ true, )) }; let substitution: Option<_> = match reference.substitution_kind { SubstitutionFunctionKind::Var => { let registration = stylist.get_custom_property_registration(&reference.name); substitution_functions .get_var(registration, &reference.name) .map(|v| Substitution::from_value(v.to_variable_value(), v.attr_tainted)) }, SubstitutionFunctionKind::Env => { let device = stylist.device(); device .environment() .get(&reference.name, device, url_data) .map(|v| Substitution::from_value(v, /* attr_tainted */ false)) }, // https://drafts.csswg.org/css-values-5/#attr-substitution SubstitutionFunctionKind::Attr => { #[cfg(feature = "gecko")] let local_name = LocalName::cast(&reference.name); #[cfg(feature = "servo")] let local_name = LocalName::from(reference.name.as_ref()); let namespace = match reference.attribute_data.namespace { ParsedNamespace::Known(ref ns) => Some(ns), ParsedNamespace::Unknown => None, }; namespace .and_then(|namespace| attribute_tracker.query(&local_name, namespace)) .map_or_else( || { // Special case when fallback and are omitted. // See FAILURE: https://drafts.csswg.org/css-values-5/#attr-substitution if reference.fallback.is_none() && reference.attribute_data.kind == AttributeType::None { simple_attr_subst("") } else { None } }, |attr| { let attr = if let AttributeType::Type(_) = &reference.attribute_data.kind { // If we're evaluating a container query, we haven't run the cascade // and populated substitution_functions.attributes, so we can't do the // get_attr() lookup here. // TODO: This means chained attr() references will not work reliably in // container style queries: // https://bugzilla.mozilla.org/show_bug.cgi?id=2028861 if computed_context.in_container_query { attr } else { substitution_functions .get_attr(&reference.name) .map(|v| v.to_variable_value())? .css } } else { attr }; let mut input = ParserInput::new(&attr); let mut parser = Parser::new(&mut input); match &reference.attribute_data.kind { AttributeType::Unit(unit) => { let css = { // Verify that attribute data is a . parser.expect_number().ok()?; let mut s = attr.clone(); s.push_str(unit.as_ref()); s }; let serialization = match unit { AttrUnit::Number => TokenSerializationType::Number, AttrUnit::Percentage => TokenSerializationType::Percentage, _ => TokenSerializationType::Dimension, }; let value = ComputedValue::new(css, url_data, serialization, serialization); Some(Substitution::from_value( value, /* attr_tainted */ true, )) }, AttributeType::Type(syntax) => { let value = SpecifiedRegisteredValue::parse( &mut parser, &syntax, url_data, None, AllowComputationallyDependent::Yes, AttrTaint::default(), ) .ok()?; let value = value.to_variable_value(); Some(Substitution::from_value( value, /* attr_tainted */ true, )) }, AttributeType::RawString | AttributeType::None => { simple_attr_subst(&attr) }, } }, ) }, }; if let Some(s) = substitution { // Skip references that are inside the outer variable (in fallback for example). while references .next_if(|next_ref| next_ref.end <= reference.end) .is_some() {} return Ok(s); } let Some(ref fallback) = reference.fallback else { return Err(()); }; do_substitute_chunk( css, fallback.start.get(), reference.end - 1, // Skip the closing parenthesis of the reference value. fallback.first_token_type, fallback.last_token_type, url_data, substitution_functions, stylist, computed_context, references, attribute_tracker, /* attr_taint */ None, ) } /// Replace `var()`, `env()`, and `attr()` functions. Return `Err(..)` for invalid at computed time. fn substitute_internal<'a>( variable_value: &'a VariableValue, substitution_functions: &'a ComputedSubstitutionFunctions, stylist: &Stylist, computed_context: &computed::Context, attribute_tracker: &mut AttributeTracker, mut attr_taint: Option<&mut AttrTaint>, ) -> Result, ()> { let mut refs = variable_value.references.refs.iter().peekable(); do_substitute_chunk( &variable_value.css, /* start = */ 0, /* end = */ variable_value.css.len(), variable_value.first_token_type, variable_value.last_token_type, &variable_value.url_data, substitution_functions, stylist, computed_context, &mut refs, attribute_tracker, attr_taint.as_deref_mut(), ) } /// Replace var(), env(), and attr() functions, returning the resulting CSS string. pub fn substitute<'a>( variable_value: &'a VariableValue, substitution_functions: &'a ComputedSubstitutionFunctions, stylist: &Stylist, computed_context: &computed::Context, attribute_tracker: &mut AttributeTracker, ) -> Result, ()> { debug_assert!(variable_value.has_references()); let mut attr_taint = AttrTaint::default(); let v = substitute_internal( variable_value, substitution_functions, stylist, computed_context, attribute_tracker, Some(&mut attr_taint), )?; Ok(SubstitutionResult { css: v.css, attr_taint, }) } ================================================ FILE: style/custom_properties_map.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The structure that contains the custom properties of a given element. use crate::custom_properties::{Name, SubstitutionFunctionKind}; use crate::properties_and_values::value::ComputedValue as ComputedRegisteredValue; use crate::selector_map::PrecomputedHasher; use indexmap::{Equivalent, IndexMap}; use rustc_hash::FxBuildHasher; use servo_arc::Arc; use std::hash::{BuildHasherDefault, Hash}; use std::sync::LazyLock; /// A map for a set of custom properties, which implements copy-on-write behavior on insertion with /// cheap copying. #[derive(Clone, Debug, PartialEq)] pub struct CustomPropertiesMap(Arc); impl Default for CustomPropertiesMap { fn default() -> Self { Self(EMPTY.clone()) } } /// We use None in the value to represent a removed entry. pub type OwnMap = IndexMap, BuildHasherDefault>; static EMPTY: LazyLock> = LazyLock::new(|| { Arc::new_leaked(Inner { own_properties: Default::default(), parent: None, len: 0, ancestor_count: 0, }) }); #[derive(Debug, Clone)] struct Inner { own_properties: OwnMap, parent: Option>, /// The number of custom properties we store. Note that this is different from the sum of our /// own and our parent's length, since we might store duplicate entries. len: usize, /// The number of ancestors we have. ancestor_count: u8, } /// A not-too-large, not too small ancestor limit, to prevent creating too-big chains. const ANCESTOR_COUNT_LIMIT: usize = 4; /// An iterator over the custom properties. pub struct Iter<'a> { current: &'a Inner, current_iter: indexmap::map::Iter<'a, Name, Option>, descendants: smallvec::SmallVec<[&'a Inner; ANCESTOR_COUNT_LIMIT]>, } impl<'a> Iterator for Iter<'a> { type Item = (&'a Name, &'a Option); fn next(&mut self) -> Option { loop { let (name, value) = match self.current_iter.next() { Some(v) => v, None => { let parent = self.current.parent.as_deref()?; self.descendants.push(self.current); self.current = parent; self.current_iter = parent.own_properties.iter(); continue; }, }; // If the property is overridden by a descendant we've already visited it. for descendant in &self.descendants { if descendant.own_properties.contains_key(name) { continue; } } return Some((name, value)); } } } impl PartialEq for Inner { fn eq(&self, other: &Self) -> bool { if self.len != other.len { return false; } // NOTE(emilio): In order to speed up custom property comparison when tons of custom // properties are involved, we return false in some cases where the ordering might be // different, but the computed values end up being the same. // // This is a performance trade-off, on the assumption that if the ordering is different, // there's likely a different value as well, but might over-invalidate. // // Doing the slow thing (checking all the keys) shows up a lot in profiles, see // bug 1926423. // // Note that self.own_properties != other.own_properties is not the same, as by default // IndexMap comparison is not order-aware. if self.own_properties.as_slice() != other.own_properties.as_slice() { return false; } self.parent == other.parent } } impl Inner { fn iter(&self) -> Iter<'_> { Iter { current: self, current_iter: self.own_properties.iter(), descendants: Default::default(), } } fn is_empty(&self) -> bool { self.len == 0 } fn len(&self) -> usize { self.len } fn get(&self, name: &Name) -> Option<&ComputedRegisteredValue> { if let Some(p) = self.own_properties.get(name) { return p.as_ref(); } self.parent.as_ref()?.get(name) } fn insert(&mut self, name: &Name, value: Option) { let new = self.own_properties.insert(name.clone(), value).is_none(); if new && self.parent.as_ref().map_or(true, |p| p.get(name).is_none()) { self.len += 1; } } /// Whether we should expand the chain, or just copy-on-write. fn should_expand_chain(&self) -> bool { const SMALL_THRESHOLD: usize = 8; if self.own_properties.len() <= SMALL_THRESHOLD { return false; // Just copy, to avoid very long chains. } self.ancestor_count < ANCESTOR_COUNT_LIMIT as u8 } } impl CustomPropertiesMap { /// Returns whether the map has no properties in it. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Returns the amount of different properties in the map. pub fn len(&self) -> usize { self.0.len() } /// Returns the property name and value at a given index. pub fn get_index(&self, index: usize) -> Option<(&Name, &Option)> { if index >= self.len() { return None; } // FIXME: This is O(n) which is a bit unfortunate. self.0.iter().nth(index) } /// Returns a given property value by name. pub fn get(&self, name: &Name) -> Option<&ComputedRegisteredValue> { self.0.get(name) } fn do_insert(&mut self, name: &Name, value: Option) { if let Some(inner) = Arc::get_mut(&mut self.0) { return inner.insert(name, value); } if self.get(name) == value.as_ref() { return; } if !self.0.should_expand_chain() { return Arc::make_mut(&mut self.0).insert(name, value); } let len = self.0.len; let ancestor_count = self.0.ancestor_count + 1; let mut new_inner = Inner { own_properties: Default::default(), // FIXME: Would be nice to avoid this clone. parent: Some(self.0.clone()), len, ancestor_count, }; new_inner.insert(name, value); self.0 = Arc::new(new_inner); } /// Inserts an element in the map. pub fn insert(&mut self, name: &Name, value: ComputedRegisteredValue) { self.do_insert(name, Some(value)) } /// Removes an element from the map. pub fn remove(&mut self, name: &Name) { self.do_insert(name, None) } /// Shrinks the map as much as possible. pub fn shrink_to_fit(&mut self) { if let Some(inner) = Arc::get_mut(&mut self.0) { inner.own_properties.shrink_to_fit() } } /// Return iterator to go through all properties. pub fn iter(&self) -> Iter<'_> { self.0.iter() } } /// An `IndexMap` containing both custom properties and attributes. #[derive(Clone, Debug, Default, PartialEq)] pub struct AllSubstitutionFunctions(IndexMap); #[derive(Clone, Debug, Hash, Eq, PartialEq)] struct Key(Name, SubstitutionFunctionKind); #[derive(Clone, Debug, Hash, Eq, PartialEq)] struct KeyRef<'a>(&'a Name, SubstitutionFunctionKind); impl<'a> Equivalent for KeyRef<'a> { fn equivalent(&self, key: &Key) -> bool { *self.0 == key.0 && self.1 == key.1 } } impl AllSubstitutionFunctions { /// Returns whether the map has zero properties and attributes in it. #[inline(always)] pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Returns a custom property or attribute value by name. #[inline(always)] pub fn get( &self, name: &Name, kind: SubstitutionFunctionKind, ) -> Option<&ComputedRegisteredValue> { debug_assert_ne!(kind, SubstitutionFunctionKind::Env); self.0.get(&KeyRef(name, kind)) } /// Inserts an element into the map. #[inline(always)] pub fn insert( &mut self, name: &Name, kind: SubstitutionFunctionKind, value: ComputedRegisteredValue, ) { debug_assert_ne!(kind, SubstitutionFunctionKind::Env); let k = Key(name.clone(), kind); self.0.insert(k, value); } /// Returns iterator to go through all substitution functions in insertion order. #[inline(always)] pub fn iter( &self, ) -> impl Iterator { self.0.iter().map(|(k, v)| (&k.0, k.1, v)) } } ================================================ FILE: style/data.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Per-node data used in style calculation. use crate::computed_value_flags::ComputedValueFlags; use crate::context::{SharedStyleContext, StackLimitChecker}; use crate::dom::TElement; use crate::invalidation::element::invalidator::InvalidationResult; use crate::invalidation::element::restyle_hints::RestyleHint; use crate::properties::ComputedValues; use crate::selector_parser::{PseudoElement, RestyleDamage, EAGER_PSEUDO_COUNT}; use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle}; #[cfg(feature = "gecko")] use malloc_size_of::MallocSizeOfOps; use selectors::matching::SelectorCaches; use servo_arc::Arc; use std::ops::{Deref, DerefMut}; use std::{fmt, mem}; #[cfg(debug_assertions)] use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; bitflags! { /// Various flags stored on ElementData. #[derive(Debug, Default)] pub struct ElementDataFlags: u8 { /// Whether the styles changed for this restyle. const WAS_RESTYLED = 1 << 0; /// Whether the last traversal of this element did not do /// any style computation. This is not true during the initial /// styling pass, nor is it true when we restyle (in which case /// WAS_RESTYLED is set). /// /// This bit always corresponds to the last time the element was /// traversed, so each traversal simply updates it with the appropriate /// value. const TRAVERSED_WITHOUT_STYLING = 1 << 1; /// Whether the primary style of this element data was reused from /// another element via a rule node comparison. This allows us to /// differentiate between elements that shared styles because they met /// all the criteria of the style sharing cache, compared to elements /// that reused style structs via rule node identity. /// /// The former gives us stronger transitive guarantees that allows us to /// apply the style sharing cache to cousins. const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2; } } /// A lazily-allocated list of styles for eagerly-cascaded pseudo-elements. /// /// We use an Arc so that sharing these styles via the style sharing cache does /// not require duplicate allocations. We leverage the copy-on-write semantics of /// Arc::make_mut(), which is free (i.e. does not require atomic RMU operations) /// in servo_arc. #[derive(Clone, Debug, Default)] pub struct EagerPseudoStyles(Option>); #[derive(Default)] struct EagerPseudoArray(EagerPseudoArrayInner); type EagerPseudoArrayInner = [Option>; EAGER_PSEUDO_COUNT]; impl Deref for EagerPseudoArray { type Target = EagerPseudoArrayInner; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for EagerPseudoArray { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } // Manually implement `Clone` here because the derived impl of `Clone` for // array types assumes the value inside is `Copy`. impl Clone for EagerPseudoArray { fn clone(&self) -> Self { let mut clone = Self::default(); for i in 0..EAGER_PSEUDO_COUNT { clone[i] = self.0[i].clone(); } clone } } // Override Debug to print which pseudos we have, and substitute the rule node // for the much-more-verbose ComputedValues stringification. impl fmt::Debug for EagerPseudoArray { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "EagerPseudoArray {{ ")?; for i in 0..EAGER_PSEUDO_COUNT { if let Some(ref values) = self[i] { write!( f, "{:?}: {:?}, ", PseudoElement::from_eager_index(i), &values.rules )?; } } write!(f, "}}") } } // Can't use [None; EAGER_PSEUDO_COUNT] here because it complains // about Copy not being implemented for our Arc type. const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None]; impl EagerPseudoStyles { /// Returns whether there are any pseudo styles. pub fn is_empty(&self) -> bool { self.0.is_none() } /// Grabs a reference to the list of styles, if they exist. pub fn as_optional_array(&self) -> Option<&EagerPseudoArrayInner> { match self.0 { None => None, Some(ref x) => Some(&x.0), } } /// Grabs a reference to the list of styles or a list of None if /// there are no styles to be had. pub fn as_array(&self) -> &EagerPseudoArrayInner { self.as_optional_array().unwrap_or(EMPTY_PSEUDO_ARRAY) } /// Returns a reference to the style for a given eager pseudo, if it exists. pub fn get(&self, pseudo: &PseudoElement) -> Option<&Arc> { debug_assert!(pseudo.is_eager()); self.0 .as_ref() .and_then(|p| p[pseudo.eager_index()].as_ref()) } /// Sets the style for the eager pseudo. pub fn set(&mut self, pseudo: &PseudoElement, value: Arc) { if self.0.is_none() { self.0 = Some(Arc::new(Default::default())); } let arr = Arc::make_mut(self.0.as_mut().unwrap()); arr[pseudo.eager_index()] = Some(value); } } /// The styles associated with a node, including the styles for any /// pseudo-elements. #[derive(Clone, Default)] pub struct ElementStyles { /// The element's style. pub primary: Option>, /// A list of the styles for the element's eagerly-cascaded pseudo-elements. pub pseudos: EagerPseudoStyles, } // There's one of these per rendered elements so it better be small. size_of_test!(ElementStyles, 16); /// Information on how this element uses viewport units. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ViewportUnitUsage { /// No viewport units are used. None = 0, /// There are viewport units used from regular style rules (which means we /// should re-cascade). FromDeclaration, /// There are viewport units used from container queries (which means we /// need to re-selector-match). FromQuery, } impl ElementStyles { /// Returns the primary style. pub fn get_primary(&self) -> Option<&Arc> { self.primary.as_ref() } /// Returns the primary style. Panic if no style available. pub fn primary(&self) -> &Arc { self.primary.as_ref().unwrap() } /// Whether this element `display` value is `none`. pub fn is_display_none(&self) -> bool { self.primary().get_box().clone_display().is_none() } /// Whether this element uses viewport units. pub fn viewport_unit_usage(&self) -> ViewportUnitUsage { fn usage_from_flags(flags: ComputedValueFlags) -> ViewportUnitUsage { if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES) { return ViewportUnitUsage::FromQuery; } if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS) { return ViewportUnitUsage::FromDeclaration; } ViewportUnitUsage::None } let primary = self.primary(); let mut usage = usage_from_flags(primary.flags); // Check cached lazy pseudos on the primary style. primary.each_cached_lazy_pseudo(|style| { usage = std::cmp::max(usage, usage_from_flags(style.flags)); }); for pseudo_style in self.pseudos.as_array() { if let Some(ref pseudo_style) = pseudo_style { usage = std::cmp::max(usage, usage_from_flags(pseudo_style.flags)); // Also check cached lazy pseudos on eager pseudo styles. pseudo_style.each_cached_lazy_pseudo(|style| { usage = std::cmp::max(usage, usage_from_flags(style.flags)); }); } } usage } #[cfg(feature = "gecko")] fn size_of_excluding_cvs(&self, _ops: &mut MallocSizeOfOps) -> usize { // As the method name suggests, we don't measures the ComputedValues // here, because they are measured on the C++ side. // XXX: measure the EagerPseudoArray itself, but not the ComputedValues // within it. 0 } } // We manually implement Debug for ElementStyles so that we can avoid the // verbose stringification of every property in the ComputedValues. We // substitute the rule node instead. impl fmt::Debug for ElementStyles { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "ElementStyles {{ primary: {:?}, pseudos: {:?} }}", self.primary.as_ref().map(|x| &x.rules), self.pseudos ) } } /// Style system data associated with an Element. /// /// In Gecko, this hangs directly off the Element. Servo, this is embedded /// inside of layout data, which itself hangs directly off the Element. In /// both cases, it is wrapped inside an AtomicRefCell to ensure thread safety. #[derive(Debug, Default)] pub struct ElementData { /// The styles for the element and its pseudo-elements. pub styles: ElementStyles, /// The restyle damage, indicating what kind of layout changes are required /// afte restyling. pub damage: RestyleDamage, /// The restyle hint, which indicates whether selectors need to be rematched /// for this element, its children, and its descendants. pub hint: RestyleHint, /// Flags. pub flags: ElementDataFlags, } /// A struct that wraps ElementData, giving it the ability of doing thread-safety checks. #[derive(Debug, Default)] pub struct ElementDataWrapper { inner: std::cell::UnsafeCell, /// Implements optional (debug_assertions-only) thread-safety checking. #[cfg(debug_assertions)] refcell: AtomicRefCell<()>, } /// A read-only reference to ElementData. #[derive(Debug)] pub struct ElementDataMut<'a> { v: &'a mut ElementData, #[cfg(debug_assertions)] _borrow: AtomicRefMut<'a, ()>, } /// A mutable reference to ElementData. #[derive(Debug)] pub struct ElementDataRef<'a> { v: &'a ElementData, #[cfg(debug_assertions)] _borrow: AtomicRef<'a, ()>, } impl ElementDataWrapper { /// Gets a non-exclusive reference to this ElementData. #[inline(always)] pub fn borrow(&self) -> ElementDataRef<'_> { #[cfg(debug_assertions)] let borrow = self.refcell.borrow(); ElementDataRef { v: unsafe { &*self.inner.get() }, #[cfg(debug_assertions)] _borrow: borrow, } } /// Gets an exclusive reference to this ElementData. #[inline(always)] pub fn borrow_mut(&self) -> ElementDataMut<'_> { #[cfg(debug_assertions)] let borrow = self.refcell.borrow_mut(); ElementDataMut { v: unsafe { &mut *self.inner.get() }, #[cfg(debug_assertions)] _borrow: borrow, } } } impl<'a> Deref for ElementDataRef<'a> { type Target = ElementData; #[inline] fn deref(&self) -> &Self::Target { &*self.v } } impl<'a> Deref for ElementDataMut<'a> { type Target = ElementData; #[inline] fn deref(&self) -> &Self::Target { &*self.v } } impl<'a> DerefMut for ElementDataMut<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.v } } // There's one of these per rendered elements so it better be small. size_of_test!(ElementData, 24); /// The kind of restyle that a single element should do. #[derive(Debug)] pub enum RestyleKind { /// We need to run selector matching plus re-cascade, that is, a full /// restyle. MatchAndCascade, /// We need to recascade with some replacement rule, such as the style /// attribute, or animation rules. CascadeWithReplacements(RestyleHint), /// We only need to recascade, for example, because only inherited /// properties in the parent changed. CascadeOnly, } fn needs_to_match_self(hint: RestyleHint, style: &ComputedValues) -> bool { if hint.intersects(RestyleHint::RESTYLE_SELF) { return true; } if hint.intersects(RestyleHint::RESTYLE_SELF_IF_PSEUDO) && style.is_pseudo_style() { return true; } if hint.intersects(RestyleHint::RESTYLE_IF_AFFECTED_BY_ANCESTOR_FONT_METRICS) && style .flags .contains(ComputedValueFlags::DEPENDS_ON_FONT_METRICS_IN_CONTAINER_QUERY) { return true; } hint.intersects( RestyleHint::RESTYLE_IF_AFFECTED_BY_STYLE_QUERIES | RestyleHint::RESTYLE_IF_AFFECTED_BY_NAMED_STYLE_CONTAINER, ) && style .flags .contains(ComputedValueFlags::DEPENDS_ON_CONTAINER_STYLE_QUERY) } impl ElementData { /// Invalidates style for this element, its descendants, and later siblings, /// based on the snapshot of the element that we took when attributes or /// state changed. pub fn invalidate_style_if_needed<'a, E: TElement>( &mut self, element: E, shared_context: &SharedStyleContext, stack_limit_checker: Option<&StackLimitChecker>, selector_caches: &'a mut SelectorCaches, ) -> InvalidationResult { // In animation-only restyle we shouldn't touch snapshot at all. if shared_context.traversal_flags.for_animation_only() { return InvalidationResult::empty(); } use crate::invalidation::element::invalidator::TreeStyleInvalidator; use crate::invalidation::element::state_and_attributes::StateAndAttrInvalidationProcessor; debug!( "invalidate_style_if_needed: {:?}, flags: {:?}, has_snapshot: {}, \ handled_snapshot: {}, pseudo: {:?}", element, shared_context.traversal_flags, element.has_snapshot(), element.handled_snapshot(), element.implemented_pseudo_element() ); if !element.has_snapshot() || element.handled_snapshot() { return InvalidationResult::empty(); } let mut processor = StateAndAttrInvalidationProcessor::new(shared_context, element, self, selector_caches); let invalidator = TreeStyleInvalidator::new(element, stack_limit_checker, &mut processor); let result = invalidator.invalidate(); unsafe { element.set_handled_snapshot() } debug_assert!(element.handled_snapshot()); result } /// Returns true if this element has styles. #[inline] pub fn has_styles(&self) -> bool { self.styles.primary.is_some() } /// Returns this element's styles as resolved styles to use for sharing. pub fn share_styles(&self) -> ResolvedElementStyles { ResolvedElementStyles { primary: self.share_primary_style(), pseudos: self.styles.pseudos.clone(), } } /// Returns this element's primary style as a resolved style to use for sharing. pub fn share_primary_style(&self) -> PrimaryStyle { let reused_via_rule_node = self .flags .contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); PrimaryStyle { style: ResolvedStyle(self.styles.primary().clone()), reused_via_rule_node, } } /// Return a copy of the element's primary style as a resolved style with the /// given flags. pub fn clone_style_with_flags(&self, flags: ComputedValueFlags) -> ResolvedStyle { let primary_style = self.styles.primary(); // We are only using this pseudo to find the correct pseudo type so it // does not matter it technically belongs to a different style. let pseudo = primary_style.pseudo(); ResolvedStyle( primary_style .deref() .clone_with_flags(flags, pseudo.as_ref()), ) } /// Sets a new set of styles, returning the old ones. pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles { self.flags.set( ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, new_styles.primary.reused_via_rule_node, ); mem::replace(&mut self.styles, new_styles.into()) } /// Returns the kind of restyling that we're going to need to do on this /// element, based of the stored restyle hint. pub fn restyle_kind(&self, shared_context: &SharedStyleContext) -> Option { let style = match self.styles.primary { Some(ref s) => s, None => return Some(RestyleKind::MatchAndCascade), }; if shared_context.traversal_flags.for_animation_only() { return self.restyle_kind_for_animation(shared_context); } let hint = self.hint; if hint.is_empty() { return None; } if needs_to_match_self(hint, style) { return Some(RestyleKind::MatchAndCascade); } if hint.has_replacements() { debug_assert!( !hint.has_animation_hint(), "Animation only restyle hint should have already processed" ); return Some(RestyleKind::CascadeWithReplacements( hint & RestyleHint::replacements(), )); } let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && style .flags .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); if needs_to_recascade_self { return Some(RestyleKind::CascadeOnly); } None } /// Returns the kind of restyling for animation-only restyle. fn restyle_kind_for_animation( &self, shared_context: &SharedStyleContext, ) -> Option { debug_assert!(shared_context.traversal_flags.for_animation_only()); debug_assert!(self.has_styles()); // FIXME: We should ideally restyle here, but it is a hack to work around our weird // animation-only traversal stuff: If we're display: none and the rules we could // match could change, we consider our style up-to-date. This is because re-cascading with // and old style doesn't guarantee returning the correct animation style (that's // bug 1393323). So if our display changed, and it changed from display: none, we would // incorrectly forget about it and wouldn't be able to correctly style our descendants // later. // XXX Figure out if this still makes sense. let hint = self.hint; if self.styles.is_display_none() && hint.intersects(RestyleHint::RESTYLE_SELF) { return None; } let style = self.styles.primary(); // Return either CascadeWithReplacements or CascadeOnly in case of animation-only restyle. // I.e. animation-only restyle never does selector matching. if hint.has_animation_hint() { return Some(RestyleKind::CascadeWithReplacements( hint & RestyleHint::for_animations(), )); } let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && style .flags .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); if needs_to_recascade_self { return Some(RestyleKind::CascadeOnly); } return None; } /// Drops any restyle state from the element. /// /// FIXME(bholley): The only caller of this should probably just assert that the hint is empty /// and call clear_flags_and_damage(). #[inline] pub fn clear_restyle_state(&mut self) { self.hint = RestyleHint::empty(); self.clear_restyle_flags_and_damage(); } /// Drops restyle flags and damage from the element. #[inline] pub fn clear_restyle_flags_and_damage(&mut self) { self.damage = RestyleDamage::empty(); self.flags.remove(ElementDataFlags::WAS_RESTYLED); } /// Mark this element as restyled, which is useful to know whether we need /// to do a post-traversal. pub fn set_restyled(&mut self) { self.flags.insert(ElementDataFlags::WAS_RESTYLED); self.flags .remove(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); } /// Returns true if this element was restyled. #[inline] pub fn is_restyle(&self) -> bool { self.flags.contains(ElementDataFlags::WAS_RESTYLED) } /// Mark that we traversed this element without computing any style for it. pub fn set_traversed_without_styling(&mut self) { self.flags .insert(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); } /// Returns whether this element has been part of a restyle. #[inline] pub fn contains_restyle_data(&self) -> bool { self.is_restyle() || !self.hint.is_empty() || !self.damage.is_empty() } /// Returns whether it is safe to perform cousin sharing based on the ComputedValues /// identity of the primary style in this ElementData. There are a few subtle things /// to check. /// /// First, if a parent element was already styled and we traversed past it without /// restyling it, that may be because our clever invalidation logic was able to prove /// that the styles of that element would remain unchanged despite changes to the id /// or class attributes. However, style sharing relies on the strong guarantee that all /// the classes and ids up the respective parent chains are identical. As such, if we /// skipped styling for one (or both) of the parents on this traversal, we can't share /// styles across cousins. Note that this is a somewhat conservative check. We could /// tighten it by having the invalidation logic explicitly flag elements for which it /// ellided styling. /// /// Second, we want to only consider elements whose ComputedValues match due to a hit /// in the style sharing cache, rather than due to the rule-node-based reuse that /// happens later in the styling pipeline. The former gives us the stronger guarantees /// we need for style sharing, the latter does not. pub fn safe_for_cousin_sharing(&self) -> bool { if self.flags.intersects( ElementDataFlags::TRAVERSED_WITHOUT_STYLING | ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, ) { return false; } if !self .styles .primary() .get_box() .clone_container_type() .is_normal() { return false; } true } /// Measures memory usage. #[cfg(feature = "gecko")] pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize { let n = self.styles.size_of_excluding_cvs(ops); // We may measure more fields in the future if DMD says it's worth it. n } } ================================================ FILE: style/device/gecko.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Gecko-specific logic for [`Device`]. use crate::color::AbsoluteColor; use crate::context::QuirksMode; use crate::custom_properties::CssEnvironment; use crate::device::Device; use crate::font_metrics::FontMetrics; use crate::gecko::wrapper::GeckoElement; use crate::gecko_bindings::bindings; use crate::gecko_bindings::structs; use crate::logical_geometry::WritingMode; use crate::media_queries::MediaType; use crate::properties::ComputedValues; use crate::string_cache::Atom; use crate::values::computed::font::GenericFontFamily; use crate::values::computed::{ColorScheme, Length, NonNegativeLength}; use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; use crate::values::specified::font::{ QueryFontMetricsFlags, FONT_MEDIUM_CAP_PX, FONT_MEDIUM_CH_PX, FONT_MEDIUM_EX_PX, FONT_MEDIUM_IC_PX, FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX, }; use crate::values::specified::ViewportVariant; use crate::values::{CustomIdent, KeyframesName}; use app_units::{Au, AU_PER_PX}; use euclid::default::Size2D; use euclid::{Scale, SideOffsets2D}; use parking_lot::RwLock; use servo_arc::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::{cmp, fmt}; use style_traits::{CSSPixel, DevicePixel}; pub(super) struct ExtraDeviceData { /// NB: The document owns the styleset, who owns the stylist, and thus the /// `Device`, so having a raw document pointer here is fine. document: *const structs::Document, } unsafe impl Sync for Device {} unsafe impl Send for Device {} impl Device { /// Trivially constructs a new `Device`. pub fn new(document: *const structs::Document) -> Self { assert!(!document.is_null()); let doc = unsafe { &*document }; let prefs = unsafe { &*bindings::Gecko_GetPrefSheetPrefs(doc) }; let default_values = ComputedValues::default_values(doc); let root_style = RwLock::new(Arc::clone(&default_values)); Device { default_values: default_values, root_style: root_style, root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), root_font_metrics_ex: AtomicU32::new(FONT_MEDIUM_EX_PX.to_bits()), root_font_metrics_cap: AtomicU32::new(FONT_MEDIUM_CAP_PX.to_bits()), root_font_metrics_ch: AtomicU32::new(FONT_MEDIUM_CH_PX.to_bits()), root_font_metrics_ic: AtomicU32::new(FONT_MEDIUM_IC_PX.to_bits()), used_root_font_size: AtomicBool::new(false), used_root_line_height: AtomicBool::new(false), used_root_font_metrics: RwLock::new(false), used_font_metrics: AtomicBool::new(false), used_viewport_size: AtomicBool::new(false), used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, // This gets updated when we see the , so it doesn't really // matter which color-scheme we look at here. body_text_color: AtomicU32::new(prefs.mLightColors.mDefault), extra: ExtraDeviceData { document }, } } /// Returns the computed line-height for the font in a given computed values instance. /// /// If you pass down an element, then the used line-height is returned. pub fn calc_line_height( &self, font: &crate::properties::style_structs::Font, writing_mode: WritingMode, element: Option, ) -> NonNegativeLength { let pres_context = self.pres_context(); let line_height = font.clone_line_height(); let au = Au(unsafe { bindings::Gecko_CalcLineHeight( &line_height, pres_context.map_or(std::ptr::null(), |pc| pc), writing_mode.is_text_vertical(), &**font, element.map_or(std::ptr::null(), |e| e.0), ) }); NonNegativeLength::new(au.to_f32_px()) } /// Whether any animation name may be referenced from the style of any /// element. pub fn animation_name_may_be_referenced(&self, name: &KeyframesName) -> bool { let pc = match self.pres_context() { Some(pc) => pc, None => return false, }; unsafe { bindings::Gecko_AnimationNameMayBeReferencedFromStyle(pc, name.as_atom().as_ptr()) } } /// The quirks mode of the document. pub fn quirks_mode(&self) -> QuirksMode { self.document().mCompatMode.into() } /// Gets the base size given a generic font family and a language. pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length { unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) } } /// Gets the size of the scrollbar in CSS pixels. pub fn scrollbar_inline_size(&self) -> Length { let pc = match self.pres_context() { Some(pc) => pc, // XXX: we could have a more reasonable default perhaps. None => return Length::new(0.0), }; Length::new(unsafe { bindings::Gecko_GetScrollbarInlineSize(pc) }) } /// Queries font metrics pub fn query_font_metrics( &self, vertical: bool, font: &crate::properties::style_structs::Font, base_size: Length, flags: QueryFontMetricsFlags, track_usage: bool, ) -> FontMetrics { if track_usage { self.used_font_metrics.store(true, Ordering::Relaxed); } let pc = match self.pres_context() { Some(pc) => pc, None => return Default::default(), }; let gecko_metrics = unsafe { bindings::Gecko_GetFontMetrics(pc, vertical, &**font, base_size, flags) }; FontMetrics { x_height: Some(gecko_metrics.mXSize), zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. { Some(gecko_metrics.mChSize) } else { None }, cap_height: if gecko_metrics.mCapHeight.px() >= 0. { Some(gecko_metrics.mCapHeight) } else { None }, ic_width: if gecko_metrics.mIcWidth.px() >= 0. { Some(gecko_metrics.mIcWidth) } else { None }, ascent: gecko_metrics.mAscent, script_percent_scale_down: if gecko_metrics.mScriptPercentScaleDown > 0. { Some(gecko_metrics.mScriptPercentScaleDown) } else { None }, script_script_percent_scale_down: if gecko_metrics.mScriptScriptPercentScaleDown > 0. { Some(gecko_metrics.mScriptScriptPercentScaleDown) } else { None }, } } /// Gets the document pointer. #[inline] pub fn document(&self) -> &structs::Document { unsafe { &*self.extra.document } } /// Gets the pres context associated with this document. #[inline] pub fn pres_context(&self) -> Option<&structs::nsPresContext> { unsafe { self.document() .mPresShell .as_ref()? .mPresContext .mRawPtr .as_ref() } } /// Gets the preference stylesheet prefs for our document. #[inline] pub fn pref_sheet_prefs(&self) -> &structs::PreferenceSheet_Prefs { unsafe { &*bindings::Gecko_GetPrefSheetPrefs(self.document()) } } /// Recreates the default computed values. pub fn reset_computed_values(&mut self) { self.default_values = ComputedValues::default_values(self.document()); } /// Rebuild all the cached data. pub fn rebuild_cached_data(&mut self) { self.reset_computed_values(); self.used_root_font_size.store(false, Ordering::Relaxed); self.used_root_line_height.store(false, Ordering::Relaxed); self.used_root_font_metrics = RwLock::new(false); self.used_font_metrics.store(false, Ordering::Relaxed); self.used_viewport_size.store(false, Ordering::Relaxed); self.used_dynamic_viewport_size .store(false, Ordering::Relaxed); } /// Recreates all the temporary state that the `Device` stores. /// /// This includes the viewport override from `@viewport` rules, and also the /// default computed values. pub fn reset(&mut self) { self.reset_computed_values(); } /// Returns whether this document is in print preview. pub fn is_print_preview(&self) -> bool { let pc = match self.pres_context() { Some(pc) => pc, None => return false, }; pc.mType == structs::nsPresContext_nsPresContextType_eContext_PrintPreview } /// Returns the current media type of the device. pub fn media_type(&self) -> MediaType { let pc = match self.pres_context() { Some(pc) => pc, None => return MediaType::screen(), }; // Gecko allows emulating random media with mMediaEmulationData.mMedium. let medium_to_use = if !pc.mMediaEmulationData.mMedium.mRawPtr.is_null() { pc.mMediaEmulationData.mMedium.mRawPtr } else { pc.mMedium as *const structs::nsAtom as *mut _ }; MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) })) } // It may make sense to account for @page rule margins here somehow, however // it's not clear how that'd work, see: // https://github.com/w3c/csswg-drafts/issues/5437 fn page_size_minus_default_margin(&self, pc: &structs::nsPresContext) -> Size2D { debug_assert!(pc.mIsRootPaginatedDocument() != 0); let area = &pc.mPageSize; let margin = &pc.mDefaultPageMargin; let width = area.width - margin.left - margin.right; let height = area.height - margin.top - margin.bottom; Size2D::new(Au(cmp::max(width, 0)), Au(cmp::max(height, 0))) } /// Returns the current viewport size in app units. pub fn au_viewport_size(&self) -> Size2D { let pc = match self.pres_context() { Some(pc) => pc, None => return Size2D::new(Au(0), Au(0)), }; if pc.mIsRootPaginatedDocument() != 0 { return self.page_size_minus_default_margin(pc); } let area = &pc.mVisibleArea; Size2D::new(Au(area.width), Au(area.height)) } /// Returns the current viewport size in app units, recording that it's been /// used for viewport unit resolution. pub fn au_viewport_size_for_viewport_unit_resolution( &self, variant: ViewportVariant, ) -> Size2D { self.used_viewport_size.store(true, Ordering::Relaxed); let pc = match self.pres_context() { Some(pc) => pc, None => return Size2D::new(Au(0), Au(0)), }; if pc.mIsRootPaginatedDocument() != 0 { return self.page_size_minus_default_margin(pc); } match variant { ViewportVariant::UADefault => { let size = &pc.mSizeForViewportUnits; Size2D::new(Au(size.width), Au(size.height)) }, ViewportVariant::Small => { let size = &pc.mVisibleArea; Size2D::new(Au(size.width), Au(size.height)) }, ViewportVariant::Large => { let size = &pc.mVisibleArea; // Looks like IntCoordTyped is treated as if it's u32 in Rust. debug_assert!( /* pc.mDynamicToolbarMaxHeight >=0 && */ pc.mDynamicToolbarMaxHeight < i32::MAX as u32 ); Size2D::new( Au(size.width), Au(size.height + pc.mDynamicToolbarMaxHeight as i32 * pc.mCurAppUnitsPerDevPixel), ) }, ViewportVariant::Dynamic => { self.used_dynamic_viewport_size .store(true, Ordering::Relaxed); let size = &pc.mVisibleArea; // Looks like IntCoordTyped is treated as if it's u32 in Rust. debug_assert!( /* pc.mDynamicToolbarHeight >=0 && */ pc.mDynamicToolbarHeight < i32::MAX as u32 ); Size2D::new( Au(size.width), Au(size.height + (pc.mDynamicToolbarMaxHeight - pc.mDynamicToolbarHeight) as i32 * pc.mCurAppUnitsPerDevPixel), ) }, } } /// Returns whether visited styles are enabled. pub fn visited_styles_enabled(&self) -> bool { unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) } } /// Returns the number of app units per device pixel we're using currently. pub fn app_units_per_device_pixel(&self) -> i32 { match self.pres_context() { Some(pc) => pc.mCurAppUnitsPerDevPixel, None => AU_PER_PX, } } /// Returns app units per pixel at 1x full-zoom. fn app_units_per_device_pixel_at_unit_full_zoom(&self) -> i32 { match self.pres_context() { Some(pc) => unsafe { (*pc.mDeviceContext.mRawPtr).mAppUnitsPerDevPixelAtUnitFullZoom }, None => AU_PER_PX, } } /// Returns the device pixel ratio, ignoring the full zoom factor. pub fn device_pixel_ratio_ignoring_full_zoom(&self) -> Scale { let au_per_px = AU_PER_PX as f32; Scale::new(au_per_px / self.app_units_per_device_pixel_at_unit_full_zoom() as f32) } /// Returns the device pixel ratio. pub fn device_pixel_ratio(&self) -> Scale { let pc = match self.pres_context() { Some(pc) => pc, None => return Scale::new(1.), }; if pc.mMediaEmulationData.mDPPX > 0.0 { return Scale::new(pc.mMediaEmulationData.mDPPX); } let au_per_dpx = pc.mCurAppUnitsPerDevPixel as f32; let au_per_px = AU_PER_PX as f32; Scale::new(au_per_px / au_per_dpx) } /// Returns whether document colors are enabled. #[inline] pub fn forced_colors(&self) -> ForcedColors { self.pres_context() .map_or(ForcedColors::None, |pc| pc.mForcedColors) } /// Computes a system color and returns it as an nscolor. pub(crate) fn system_nscolor( &self, system_color: SystemColor, color_scheme: ColorSchemeFlags, ) -> u32 { unsafe { bindings::Gecko_ComputeSystemColor(system_color, self.document(), &color_scheme) } } /// Returns whether the used color-scheme for `color-scheme` should be dark. pub(crate) fn is_dark_color_scheme(&self, color_scheme: ColorSchemeFlags) -> bool { unsafe { bindings::Gecko_IsDarkColorScheme(self.document(), &color_scheme) } } /// Returns the default background color. /// /// This is only for forced-colors/high-contrast, so looking at light colors /// is ok. pub fn default_background_color(&self) -> AbsoluteColor { AbsoluteColor::from_nscolor( self.system_nscolor(SystemColor::Canvas, ColorScheme::normal().bits), ) } /// Returns the default foreground color. /// /// See above for looking at light colors only. pub fn default_color(&self) -> AbsoluteColor { AbsoluteColor::from_nscolor( self.system_nscolor(SystemColor::Canvastext, ColorScheme::normal().bits), ) } /// Returns the current effective text zoom. #[inline] fn text_zoom(&self) -> f32 { let pc = match self.pres_context() { Some(pc) => pc, None => return 1., }; pc.mTextZoom } /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText). #[inline] pub fn zoom_text(&self, size: Length) -> Length { size.scale_by(self.text_zoom()) } /// Un-apply text zoom. #[inline] pub fn unzoom_text(&self, size: Length) -> Length { size.scale_by(1. / self.text_zoom()) } /// Returns safe area insets pub fn safe_area_insets(&self) -> SideOffsets2D { let pc = match self.pres_context() { Some(pc) => pc, None => return SideOffsets2D::zero(), }; let mut top = 0.0; let mut right = 0.0; let mut bottom = 0.0; let mut left = 0.0; unsafe { bindings::Gecko_GetSafeAreaInsets(pc, &mut top, &mut right, &mut bottom, &mut left) }; SideOffsets2D::new(top, right, bottom, left) } /// Returns true if the given MIME type is supported pub fn is_supported_mime_type(&self, mime_type: &str) -> bool { unsafe { bindings::Gecko_IsSupportedImageMimeType(mime_type.as_ptr(), mime_type.len() as u32) } } /// Return whether the document is a chrome document. /// /// This check is consistent with how we enable chrome rules for chrome:// and resource:// /// stylesheets (and thus chrome:// documents). #[inline] pub fn chrome_rules_enabled_for_document(&self) -> bool { self.document().mChromeRulesEnabled() } } impl fmt::Debug for Device { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use nsstring::nsCString; let mut doc_uri = nsCString::new(); unsafe { bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) }; f.debug_struct("Device") .field("document_url", &doc_uri) .finish() } } ================================================ FILE: style/device/mod.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Media-query device and expression representation. use crate::color::AbsoluteColor; use crate::custom_properties::CssEnvironment; #[cfg(feature = "servo")] use crate::derives::*; use crate::properties::ComputedValues; use crate::values::computed::font::QueryFontMetricsFlags; use crate::values::computed::Length; use parking_lot::RwLock; use servo_arc::Arc; use std::mem; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; #[cfg(feature = "gecko")] use crate::device::gecko::ExtraDeviceData; #[cfg(feature = "servo")] use crate::device::servo::ExtraDeviceData; #[cfg(feature = "gecko")] pub mod gecko; #[cfg(feature = "servo")] pub mod servo; /// A device is a structure that represents the current media a given document /// is displayed in. /// /// This is the struct against which media queries are evaluated, has a default /// values computed, and contains all the viewport rule state. /// /// This structure also contains atomics used for computing root font-relative /// units. These atomics use relaxed ordering, since when computing the style /// of the root element, there can't be any other style being computed at the /// same time (given we need the style of the parent to compute everything else). /// /// In Gecko, it wraps a pres context. #[cfg_attr(feature = "servo", derive(Debug, MallocSizeOf))] pub struct Device { /// The default computed values for this Device. #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc is shared")] default_values: Arc, /// Current computed style of the root element, used for calculations of /// root font-relative units. #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] root_style: RwLock>, /// Font size of the root element, used for rem units in other elements. root_font_size: AtomicU32, /// Line height of the root element, used for rlh units in other elements. root_line_height: AtomicU32, /// X-height of the root element, used for rex units in other elements. root_font_metrics_ex: AtomicU32, /// Cap-height of the root element, used for rcap units in other elements. root_font_metrics_cap: AtomicU32, /// Advance measure (ch) of the root element, used for rch units in other elements. root_font_metrics_ch: AtomicU32, /// Ideographic advance measure of the root element, used for ric units in other elements. root_font_metrics_ic: AtomicU32, /// Whether any styles computed in the document relied on the root font-size /// by using rem units. used_root_font_size: AtomicBool, /// Whether any styles computed in the document relied on the root line-height /// by using rlh units. used_root_line_height: AtomicBool, /// Whether any styles computed in the document relied on the root font metrics /// by using rcap, rch, rex, or ric units. This is a lock instead of an atomic /// in order to prevent concurrent writes to the root font metric values. #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Pure stack type")] used_root_font_metrics: RwLock, /// Whether any styles computed in the document relied on font metrics. used_font_metrics: AtomicBool, /// Whether any styles computed in the document relied on the viewport size /// by using vw/vh/vmin/vmax units. used_viewport_size: AtomicBool, /// Whether any styles computed in the document relied on the viewport size /// by using dvw/dvh/dvmin/dvmax units. used_dynamic_viewport_size: AtomicBool, /// The CssEnvironment object responsible of getting CSS environment /// variables. environment: CssEnvironment, /// The body text color, stored as an `nscolor`, used for the "tables /// inherit from body" quirk. /// /// body_text_color: AtomicU32, /// Extra Gecko-specific or Servo-specific data. extra: ExtraDeviceData, } impl Device { /// Get the relevant environment to resolve `env()` functions. #[inline] pub fn environment(&self) -> &CssEnvironment { &self.environment } /// Returns the default computed values as a reference, in order to match /// Servo. pub fn default_computed_values(&self) -> &ComputedValues { &self.default_values } /// Returns the default computed values as an `Arc`. pub fn default_computed_values_arc(&self) -> &Arc { &self.default_values } /// Store a pointer to the root element's computed style, for use in /// calculation of root font-relative metrics. pub fn set_root_style(&self, style: &Arc) { *self.root_style.write() = style.clone(); } /// Get the font size of the root element (for rem) pub fn root_font_size(&self) -> Length { self.used_root_font_size.store(true, Ordering::Relaxed); Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) } /// Set the font size of the root element (for rem), in zoom-independent CSS pixels. pub fn set_root_font_size(&self, size: f32) { self.root_font_size.store(size.to_bits(), Ordering::Relaxed) } /// Get the line height of the root element (for rlh) pub fn root_line_height(&self) -> Length { self.used_root_line_height.store(true, Ordering::Relaxed); Length::new(f32::from_bits( self.root_line_height.load(Ordering::Relaxed), )) } /// Set the line height of the root element (for rlh), in zoom-independent CSS pixels. pub fn set_root_line_height(&self, size: f32) { self.root_line_height .store(size.to_bits(), Ordering::Relaxed); } /// Get the x-height of the root element (for rex) pub fn root_font_metrics_ex(&self) -> Length { self.ensure_root_font_metrics_updated(); Length::new(f32::from_bits( self.root_font_metrics_ex.load(Ordering::Relaxed), )) } /// Set the x-height of the root element (for rex), in zoom-independent CSS pixels. pub fn set_root_font_metrics_ex(&self, size: f32) -> bool { let size = size.to_bits(); let previous = self.root_font_metrics_ex.swap(size, Ordering::Relaxed); previous != size } /// Get the cap-height of the root element (for rcap) pub fn root_font_metrics_cap(&self) -> Length { self.ensure_root_font_metrics_updated(); Length::new(f32::from_bits( self.root_font_metrics_cap.load(Ordering::Relaxed), )) } /// Set the cap-height of the root element (for rcap), in zoom-independent CSS pixels. pub fn set_root_font_metrics_cap(&self, size: f32) -> bool { let size = size.to_bits(); let previous = self.root_font_metrics_cap.swap(size, Ordering::Relaxed); previous != size } /// Get the advance measure of the root element (for rch) pub fn root_font_metrics_ch(&self) -> Length { self.ensure_root_font_metrics_updated(); Length::new(f32::from_bits( self.root_font_metrics_ch.load(Ordering::Relaxed), )) } /// Set the advance measure of the root element (for rch), in zoom-independent CSS pixels. pub fn set_root_font_metrics_ch(&self, size: f32) -> bool { let size = size.to_bits(); let previous = self.root_font_metrics_ch.swap(size, Ordering::Relaxed); previous != size } /// Get the ideographic advance measure of the root element (for ric) pub fn root_font_metrics_ic(&self) -> Length { self.ensure_root_font_metrics_updated(); Length::new(f32::from_bits( self.root_font_metrics_ic.load(Ordering::Relaxed), )) } /// Set the ideographic advance measure of the root element (for ric), in zoom-independent CSS pixels. pub fn set_root_font_metrics_ic(&self, size: f32) -> bool { let size = size.to_bits(); let previous = self.root_font_metrics_ic.swap(size, Ordering::Relaxed); previous != size } fn ensure_root_font_metrics_updated(&self) { let mut guard = self.used_root_font_metrics.write(); let previously_computed = mem::replace(&mut *guard, true); if !previously_computed { self.update_root_font_metrics(); } } /// Compute the root element's font metrics, and returns a bool indicating whether /// the font metrics have changed since the previous restyle. pub fn update_root_font_metrics(&self) -> bool { let root_style = self.root_style.read(); let root_effective_zoom = (*root_style).effective_zoom; let root_font_size = (*root_style).get_font().clone_font_size().computed_size(); let root_font_metrics = self.query_font_metrics( (*root_style).writing_mode.is_upright(), &(*root_style).get_font(), root_font_size, QueryFontMetricsFlags::USE_USER_FONT_SET | QueryFontMetricsFlags::NEEDS_CH | QueryFontMetricsFlags::NEEDS_IC, /* track_usage = */ false, ); let mut root_font_metrics_changed = false; root_font_metrics_changed |= self.set_root_font_metrics_ex( root_effective_zoom.unzoom(root_font_metrics.x_height_or_default(root_font_size).px()), ); root_font_metrics_changed |= self.set_root_font_metrics_ch( root_effective_zoom.unzoom( root_font_metrics .zero_advance_measure_or_default( root_font_size, (*root_style).writing_mode.is_upright(), ) .px(), ), ); root_font_metrics_changed |= self.set_root_font_metrics_cap( root_effective_zoom.unzoom(root_font_metrics.cap_height_or_default().px()), ); root_font_metrics_changed |= self.set_root_font_metrics_ic( root_effective_zoom.unzoom(root_font_metrics.ic_width_or_default(root_font_size).px()), ); root_font_metrics_changed } /// Returns whether we ever looked up the root font size of the Device. pub fn used_root_font_size(&self) -> bool { self.used_root_font_size.load(Ordering::Relaxed) } /// Returns whether we ever looked up the root line-height of the device. pub fn used_root_line_height(&self) -> bool { self.used_root_line_height.load(Ordering::Relaxed) } /// Returns whether we ever looked up the root font metrics of the device. pub fn used_root_font_metrics(&self) -> bool { *self.used_root_font_metrics.read() } /// Returns whether we ever looked up the viewport size of the Device. pub fn used_viewport_size(&self) -> bool { self.used_viewport_size.load(Ordering::Relaxed) } /// Returns whether we ever looked up the dynamic viewport size of the Device. pub fn used_dynamic_viewport_size(&self) -> bool { self.used_dynamic_viewport_size.load(Ordering::Relaxed) } /// Returns whether font metrics have been queried. pub fn used_font_metrics(&self) -> bool { self.used_font_metrics.load(Ordering::Relaxed) } /// Returns the body text color. pub fn body_text_color(&self) -> AbsoluteColor { AbsoluteColor::from_nscolor(self.body_text_color.load(Ordering::Relaxed)) } /// Sets the body text color for the "inherit color from body" quirk. /// /// pub fn set_body_text_color(&self, color: AbsoluteColor) { self.body_text_color .store(color.to_nscolor(), Ordering::Relaxed) } } ================================================ FILE: style/device/servo.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Servo-specific logic for [`Device`]. use crate::color::AbsoluteColor; use crate::context::QuirksMode; use crate::custom_properties::CssEnvironment; use crate::font_metrics::FontMetrics; use crate::logical_geometry::WritingMode; use crate::media_queries::MediaType; use crate::properties::style_structs::Font; use crate::properties::ComputedValues; use crate::queries::values::PrefersColorScheme; use crate::values::computed::font::GenericFontFamily; use crate::values::computed::{CSSPixelLength, Length, LineHeight, NonNegativeLength}; use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; use crate::values::specified::font::{ QueryFontMetricsFlags, FONT_MEDIUM_CAP_PX, FONT_MEDIUM_CH_PX, FONT_MEDIUM_EX_PX, FONT_MEDIUM_IC_PX, FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX, }; use crate::values::specified::ViewportVariant; use crate::values::KeyframesName; use app_units::{Au, AU_PER_PX}; use euclid::default::Size2D as UntypedSize2D; use euclid::{Scale, SideOffsets2D, Size2D}; use malloc_size_of_derive::MallocSizeOf; use mime::Mime; use parking_lot::RwLock; use servo_arc::Arc; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use style_traits::{CSSPixel, DevicePixel}; use crate::device::Device; /// A trait used to query font metrics in clients of Stylo. This is used by Device to /// query font metrics in a way that is specific to the client using Stylo. pub trait FontMetricsProvider: Debug + Sync { /// Query the font metrics for the given font and the given base font size. fn query_font_metrics( &self, vertical: bool, font: &Font, base_size: CSSPixelLength, flags: QueryFontMetricsFlags, ) -> FontMetrics; /// Gets the base size given a generic font family. fn base_size_for_generic(&self, generic: GenericFontFamily) -> Length; } #[derive(Debug, MallocSizeOf)] pub(super) struct ExtraDeviceData { /// The current media type used by de device. media_type: MediaType, /// The current viewport size, in CSS pixels. viewport_size: Size2D, /// The current device pixel ratio, from CSS pixels to device pixels. device_pixel_ratio: Scale, /// The current quirks mode. #[ignore_malloc_size_of = "Pure stack type"] quirks_mode: QuirksMode, /// Whether the user prefers light mode or dark mode #[ignore_malloc_size_of = "Pure stack type"] prefers_color_scheme: PrefersColorScheme, /// An implementation of a trait which implements support for querying font metrics. #[ignore_malloc_size_of = "Owned by embedder"] font_metrics_provider: Box, } impl Device { /// Trivially construct a new `Device`. pub fn new( media_type: MediaType, quirks_mode: QuirksMode, viewport_size: Size2D, device_pixel_ratio: Scale, font_metrics_provider: Box, default_values: Arc, prefers_color_scheme: PrefersColorScheme, ) -> Device { let root_style = RwLock::new(Arc::clone(&default_values)); Device { root_style, root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), root_font_metrics_ex: AtomicU32::new(FONT_MEDIUM_EX_PX.to_bits()), root_font_metrics_cap: AtomicU32::new(FONT_MEDIUM_CAP_PX.to_bits()), root_font_metrics_ch: AtomicU32::new(FONT_MEDIUM_CH_PX.to_bits()), root_font_metrics_ic: AtomicU32::new(FONT_MEDIUM_IC_PX.to_bits()), used_root_font_size: AtomicBool::new(false), used_root_line_height: AtomicBool::new(false), used_root_font_metrics: RwLock::new(false), used_font_metrics: AtomicBool::new(false), used_viewport_size: AtomicBool::new(false), used_dynamic_viewport_size: AtomicBool::new(false), environment: CssEnvironment, default_values, body_text_color: AtomicU32::new(AbsoluteColor::BLACK.to_nscolor()), extra: ExtraDeviceData { media_type, viewport_size, device_pixel_ratio, quirks_mode, prefers_color_scheme, font_metrics_provider, }, } } /// Returns the computed line-height for the font in a given computed values instance. /// /// If you pass down an element, then the used line-height is returned. pub fn calc_line_height( &self, font: &crate::properties::style_structs::Font, _writing_mode: WritingMode, _element: Option<()>, ) -> NonNegativeLength { (match font.line_height { // TODO: compute `normal` from the font metrics LineHeight::Normal => CSSPixelLength::new(0.), LineHeight::Number(number) => font.font_size.computed_size() * number.0, LineHeight::Length(length) => length.0, }) .into() } /// Get the quirks mode of the current device. pub fn quirks_mode(&self) -> QuirksMode { self.extra.quirks_mode } /// Gets the base size given a generic font family. pub fn base_size_for_generic(&self, generic: GenericFontFamily) -> Length { self.extra .font_metrics_provider .base_size_for_generic(generic) } /// Whether a given animation name may be referenced from style. pub fn animation_name_may_be_referenced(&self, _: &KeyframesName) -> bool { // Assume it is, since we don't have any good way to prove it's not. true } /// Get the viewport size on this [`Device`]. pub fn viewport_size(&self) -> Size2D { self.extra.viewport_size } /// Set the viewport size on this [`Device`]. /// /// Note that this does not update any associated `Stylist`. For this you must call /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_viewport_size(&mut self, viewport_size: Size2D) { self.extra.viewport_size = viewport_size; } /// Returns the viewport size of the current device in app units, needed, /// among other things, to resolve viewport units. #[inline] pub fn au_viewport_size(&self) -> UntypedSize2D { Size2D::new( Au::from_f32_px(self.extra.viewport_size.width), Au::from_f32_px(self.extra.viewport_size.height), ) } /// Like the above, but records that we've used viewport units. pub fn au_viewport_size_for_viewport_unit_resolution( &self, _: ViewportVariant, ) -> UntypedSize2D { self.used_viewport_size.store(true, Ordering::Relaxed); // Servo doesn't have dynamic UA interfaces that affect the viewport, // so we can just ignore the ViewportVariant. self.au_viewport_size() } /// Returns the number of app units per device pixel we're using currently. pub fn app_units_per_device_pixel(&self) -> i32 { (AU_PER_PX as f32 / self.extra.device_pixel_ratio.0) as i32 } /// Returns the device pixel ratio, ignoring the full zoom factor. pub fn device_pixel_ratio_ignoring_full_zoom(&self) -> Scale { self.extra.device_pixel_ratio } /// Returns the device pixel ratio. pub fn device_pixel_ratio(&self) -> Scale { self.extra.device_pixel_ratio } /// Set a new device pixel ratio on this [`Device`]. /// /// Note that this does not update any associated `Stylist`. For this you must call /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_device_pixel_ratio( &mut self, device_pixel_ratio: Scale, ) { self.extra.device_pixel_ratio = device_pixel_ratio; } /// Gets the size of the scrollbar in CSS pixels. pub fn scrollbar_inline_size(&self) -> CSSPixelLength { // TODO: implement this. CSSPixelLength::new(0.0) } /// Queries font metrics using the [`FontMetricsProvider`] interface. pub fn query_font_metrics( &self, vertical: bool, font: &Font, base_size: CSSPixelLength, flags: QueryFontMetricsFlags, track_usage: bool, ) -> FontMetrics { if track_usage { self.used_font_metrics.store(true, Ordering::Relaxed); } self.extra .font_metrics_provider .query_font_metrics(vertical, font, base_size, flags) } /// Return the media type of the current device. pub fn media_type(&self) -> MediaType { self.extra.media_type.clone() } /// Returns whether document colors are enabled. pub fn forced_colors(&self) -> ForcedColors { ForcedColors::None } /// Returns the default background color. pub fn default_background_color(&self) -> AbsoluteColor { AbsoluteColor::WHITE } /// Returns the default foreground color. pub fn default_color(&self) -> AbsoluteColor { AbsoluteColor::BLACK } /// Set the [`PrefersColorScheme`] value on this [`Device`]. /// /// Note that this does not update any associated `Stylist`. For this you must call /// `Stylist::media_features_change_changed_style` and /// `Stylist::force_stylesheet_origins_dirty`. pub fn set_color_scheme(&mut self, new_color_scheme: PrefersColorScheme) { self.extra.prefers_color_scheme = new_color_scheme; } /// Returns the color scheme of this [`Device`]. pub fn color_scheme(&self) -> PrefersColorScheme { self.extra.prefers_color_scheme } pub(crate) fn is_dark_color_scheme(&self, _: ColorSchemeFlags) -> bool { false } pub(crate) fn system_color( &self, system_color: SystemColor, color_scheme_flags: ColorSchemeFlags, ) -> AbsoluteColor { fn srgb(r: u8, g: u8, b: u8) -> AbsoluteColor { AbsoluteColor::srgb_legacy(r, g, b, 1f32) } // Refer to spec // if self.is_dark_color_scheme(color_scheme_flags) { // Note: is_dark_color_scheme always returns true, so this code is dead code. match system_color { SystemColor::Accentcolor => srgb(10, 132, 255), SystemColor::Accentcolortext => srgb(255, 255, 255), SystemColor::Activetext => srgb(255, 0, 0), SystemColor::Linktext => srgb(158, 158, 255), SystemColor::Visitedtext => srgb(208, 173, 240), SystemColor::Buttonborder // Deprecated system colors (CSS Color 4) mapped to Buttonborder. | SystemColor::Activeborder | SystemColor::Inactiveborder | SystemColor::Threeddarkshadow | SystemColor::Threedshadow | SystemColor::Windowframe => srgb(255, 255, 255), SystemColor::Buttonface // Deprecated system colors (CSS Color 4) mapped to Buttonface. | SystemColor::Buttonhighlight | SystemColor::Buttonshadow | SystemColor::Threedface | SystemColor::Threedhighlight | SystemColor::Threedlightshadow => srgb(107, 107, 107), SystemColor::Buttontext => srgb(245, 245, 245), SystemColor::Canvas // Deprecated system colors (CSS Color 4) mapped to Canvas. | SystemColor::Activecaption | SystemColor::Appworkspace | SystemColor::Background | SystemColor::Inactivecaption | SystemColor::Infobackground | SystemColor::Menu | SystemColor::Scrollbar | SystemColor::Window => srgb(30, 30, 30), SystemColor::Canvastext // Deprecated system colors (CSS Color 4) mapped to Canvastext. | SystemColor::Captiontext | SystemColor::Infotext | SystemColor::Menutext | SystemColor::Windowtext => srgb(232, 232, 232), SystemColor::Field => srgb(45, 45, 45), SystemColor::Fieldtext => srgb(240, 240, 240), SystemColor::Graytext // Deprecated system colors (CSS Color 4) mapped to Graytext. | SystemColor::Inactivecaptiontext => srgb(155, 155, 155), SystemColor::Highlight => srgb(38, 79, 120), SystemColor::Highlighttext => srgb(255, 255, 255), SystemColor::Mark => srgb(102, 92, 0), SystemColor::Marktext => srgb(255, 255, 255), SystemColor::Selecteditem => srgb(153, 200, 255), SystemColor::Selecteditemtext => srgb(59, 59, 59), } } else { match system_color { SystemColor::Accentcolor => srgb(0, 102, 204), SystemColor::Accentcolortext => srgb(255, 255, 255), SystemColor::Activetext => srgb(238, 0, 0), SystemColor::Linktext => srgb(0, 0, 238), SystemColor::Visitedtext => srgb(85, 26, 139), SystemColor::Buttonborder // Deprecated system colors (CSS Color 4) mapped to Buttonborder. | SystemColor::Activeborder | SystemColor::Inactiveborder | SystemColor::Threeddarkshadow | SystemColor::Threedshadow | SystemColor::Windowframe => srgb(169, 169, 169), SystemColor::Buttonface // Deprecated system colors (CSS Color 4) mapped to Buttonface. | SystemColor::Buttonhighlight | SystemColor::Buttonshadow | SystemColor::Threedface | SystemColor::Threedhighlight | SystemColor::Threedlightshadow => srgb(220, 220, 220), SystemColor::Buttontext => srgb(0, 0, 0), SystemColor::Canvas // Deprecated system colors (CSS Color 4) mapped to Canvas. | SystemColor::Activecaption | SystemColor::Appworkspace | SystemColor::Background | SystemColor::Inactivecaption | SystemColor::Infobackground | SystemColor::Menu | SystemColor::Scrollbar | SystemColor::Window => srgb(255, 255, 255), SystemColor::Canvastext // Deprecated system colors (CSS Color 4) mapped to Canvastext. | SystemColor::Captiontext | SystemColor::Infotext | SystemColor::Menutext | SystemColor::Windowtext => srgb(0, 0, 0), SystemColor::Field => srgb(255, 255, 255), SystemColor::Fieldtext => srgb(0, 0, 0), SystemColor::Graytext // Deprecated system colors (CSS Color 4) mapped to Graytext. | SystemColor::Inactivecaptiontext => srgb(109, 109, 109), SystemColor::Highlight => srgb(0, 65, 198), SystemColor::Highlighttext => srgb(0, 0, 0), SystemColor::Mark => srgb(255, 235, 59), SystemColor::Marktext => srgb(0, 0, 0), SystemColor::Selecteditem => srgb(0, 102, 204), SystemColor::Selecteditemtext => srgb(255, 255, 255), } } } /// Returns safe area insets pub fn safe_area_insets(&self) -> SideOffsets2D { SideOffsets2D::zero() } /// Returns true if the given MIME type is supported pub fn is_supported_mime_type(&self, mime_type: &str) -> bool { match mime_type.parse::() { Ok(m) => { // Keep this in sync with 'image_classifer' from // components/net/mime_classifier.rs m == mime::IMAGE_BMP || m == mime::IMAGE_GIF || m == mime::IMAGE_PNG || m == mime::IMAGE_JPEG || m == "image/x-icon" || m == "image/webp" }, _ => false, } } /// Return whether the document is a chrome document. #[inline] pub fn chrome_rules_enabled_for_document(&self) -> bool { false } } ================================================ FILE: style/dom.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Types and traits used to access the DOM from style calculation. #![allow(unsafe_code)] #![deny(missing_docs)] use crate::applicable_declarations::ApplicableDeclarationBlock; use crate::context::SharedStyleContext; #[cfg(feature = "gecko")] use crate::context::UpdateAnimationsTasks; use crate::data::{ElementData, ElementDataMut, ElementDataRef}; use crate::device::Device; use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock}; use crate::selector_map::PrecomputedHashMap; use crate::selector_parser::{AttrValue, Lang, PseudoElement, RestyleDamage, SelectorImpl}; use crate::shared_lock::{Locked, SharedRwLock}; use crate::stylesheets::scope_rule::ImplicitScopeRoot; use crate::stylist::CascadeData; use crate::values::computed::Display; use crate::values::AtomIdent; use crate::{LocalName, Namespace, WeakAtom}; use dom::ElementState; use selectors::matching::{ElementSelectorFlags, QuirksMode, VisitedHandlingMode}; use selectors::sink::Push; use selectors::{Element as SelectorsElement, OpaqueElement}; use servo_arc::{Arc, ArcBorrow}; use smallvec::SmallVec; use std::fmt; use std::fmt::Debug; use std::hash::Hash; use std::ops::Deref; pub use style_traits::dom::OpaqueNode; /// Simple trait to provide basic information about the type of an element. /// /// We avoid exposing the full type id, since computing it in the general case /// would be difficult for Gecko nodes. pub trait NodeInfo { /// Whether this node is an element. fn is_element(&self) -> bool; /// Whether this node is a text node. fn is_text_node(&self) -> bool; } /// A node iterator that only returns node that don't need layout. pub struct LayoutIterator(pub T); impl Iterator for LayoutIterator where T: Iterator, N: NodeInfo, { type Item = N; fn next(&mut self) -> Option { loop { let n = self.0.next()?; // Filter out nodes that layout should ignore. if n.is_text_node() || n.is_element() { return Some(n); } } } } /// An iterator over the DOM children of a node. pub struct DomChildren(Option); impl Iterator for DomChildren where N: TNode, { type Item = N; fn next(&mut self) -> Option { let n = self.0.take()?; self.0 = n.next_sibling(); Some(n) } } /// An iterator over the DOM descendants of a node in pre-order. pub struct DomDescendants { previous: Option, scope: N, } impl DomDescendants where N: TNode, { /// Returns the next element ignoring all of our subtree. #[inline] pub fn next_skipping_children(&mut self) -> Option { let prev = self.previous.take()?; self.previous = prev.next_in_preorder_skipping_children(self.scope); self.previous } } impl Iterator for DomDescendants where N: TNode, { type Item = N; #[inline] fn next(&mut self) -> Option { let prev = self.previous.take()?; self.previous = prev.next_in_preorder(self.scope); self.previous } } /// The `TDocument` trait, to represent a document node. pub trait TDocument: Sized + Copy + Clone { /// The concrete `TNode` type. type ConcreteNode: TNode; /// Get this document as a `TNode`. fn as_node(&self) -> Self::ConcreteNode; /// Returns whether this document is an HTML document. fn is_html_document(&self) -> bool; /// Returns the quirks mode of this document. fn quirks_mode(&self) -> QuirksMode; /// Get a list of elements with a given ID in this document, sorted by /// tree position. /// /// Can return an error to signal that this list is not available, or also /// return an empty slice. fn elements_with_id<'a>( &self, _id: &AtomIdent, ) -> Result<&'a [::ConcreteElement], ()> where Self: 'a, { Err(()) } /// This document's shared lock. fn shared_lock(&self) -> &SharedRwLock; } /// The `TNode` trait. This is the main generic trait over which the style /// system can be implemented. pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { /// The concrete `TElement` type. type ConcreteElement: TElement; /// The concrete `TDocument` type. type ConcreteDocument: TDocument; /// The concrete `TShadowRoot` type. type ConcreteShadowRoot: TShadowRoot; /// Get this node's parent node. fn parent_node(&self) -> Option; /// Get this node's first child. fn first_child(&self) -> Option; /// Get this node's last child. fn last_child(&self) -> Option; /// Get this node's previous sibling. fn prev_sibling(&self) -> Option; /// Get this node's next sibling. fn next_sibling(&self) -> Option; /// Get the owner document of this node. fn owner_doc(&self) -> Self::ConcreteDocument; /// Iterate over the DOM children of a node. #[inline(always)] fn dom_children(&self) -> DomChildren { DomChildren(self.first_child()) } /// Returns whether the node is attached to a document. fn is_in_document(&self) -> bool; /// Iterate over the DOM children of a node, in preorder. #[inline(always)] fn dom_descendants(&self) -> DomDescendants { DomDescendants { previous: Some(*self), scope: *self, } } /// Returns the next node after this one, in a pre-order tree-traversal of /// the subtree rooted at scoped_to. #[inline] fn next_in_preorder(&self, scoped_to: Self) -> Option { if let Some(c) = self.first_child() { return Some(c); } self.next_in_preorder_skipping_children(scoped_to) } /// Returns the next node in tree order, skipping the children of the current node. /// /// This is useful when we know that a subtree cannot contain matches, allowing us /// to skip entire subtrees during traversal. #[inline] fn next_in_preorder_skipping_children(&self, scoped_to: Self) -> Option { let mut current = *self; loop { if current == scoped_to { return None; } if let Some(s) = current.next_sibling() { return Some(s); } debug_assert!( current.parent_node().is_some(), "Not a descendant of the scope?" ); current = current.parent_node()?; } } /// Returns the depth of this node in the DOM. fn depth(&self) -> usize { let mut depth = 0; let mut curr = *self; while let Some(parent) = curr.traversal_parent() { depth += 1; curr = parent.as_node(); } depth } /// Get this node's parent element from the perspective of a restyle /// traversal. fn traversal_parent(&self) -> Option; /// Get this node's parent element if present. fn parent_element(&self) -> Option { self.parent_node().and_then(|n| n.as_element()) } /// Get this node's parent element, or shadow host if it's a shadow root. fn parent_element_or_host(&self) -> Option { let parent = self.parent_node()?; if let Some(e) = parent.as_element() { return Some(e); } if let Some(root) = parent.as_shadow_root() { return Some(root.host()); } None } /// Converts self into an `OpaqueNode`. fn opaque(&self) -> OpaqueNode; /// A debug id, only useful, mm... for debugging. fn debug_id(self) -> usize; /// Get this node as an element, if it's one. fn as_element(&self) -> Option; /// Get this node as a document, if it's one. fn as_document(&self) -> Option; /// Get this node as a ShadowRoot, if it's one. fn as_shadow_root(&self) -> Option; } /// Wrapper to output the subtree rather than the single node when formatting /// for Debug. pub struct ShowSubtree(pub N); impl Debug for ShowSubtree { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "DOM Subtree:")?; fmt_subtree(f, &|f, n| write!(f, "{:?}", n), self.0, 1) } } /// Wrapper to output the subtree along with the ElementData when formatting /// for Debug. pub struct ShowSubtreeData(pub N); impl Debug for ShowSubtreeData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "DOM Subtree:")?; fmt_subtree(f, &|f, n| fmt_with_data(f, n), self.0, 1) } } /// Wrapper to output the subtree along with the ElementData and primary /// ComputedValues when formatting for Debug. This is extremely verbose. #[cfg(feature = "servo")] pub struct ShowSubtreeDataAndPrimaryValues(pub N); #[cfg(feature = "servo")] impl Debug for ShowSubtreeDataAndPrimaryValues { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "DOM Subtree:")?; fmt_subtree(f, &|f, n| fmt_with_data_and_primary_values(f, n), self.0, 1) } } fn fmt_with_data(f: &mut fmt::Formatter, n: N) -> fmt::Result { if let Some(el) = n.as_element() { write!( f, "{:?} dd={} aodd={} data={:?}", el, el.has_dirty_descendants(), el.has_animation_only_dirty_descendants(), el.borrow_data(), ) } else { write!(f, "{:?}", n) } } #[cfg(feature = "servo")] fn fmt_with_data_and_primary_values(f: &mut fmt::Formatter, n: N) -> fmt::Result { if let Some(el) = n.as_element() { let dd = el.has_dirty_descendants(); let aodd = el.has_animation_only_dirty_descendants(); let data = el.borrow_data(); let values = data.as_ref().and_then(|d| d.styles.get_primary()); write!( f, "{:?} dd={} aodd={} data={:?} values={:?}", el, dd, aodd, &data, values ) } else { write!(f, "{:?}", n) } } fn fmt_subtree(f: &mut fmt::Formatter, stringify: &F, n: N, indent: u32) -> fmt::Result where F: Fn(&mut fmt::Formatter, N) -> fmt::Result, { for _ in 0..indent { write!(f, " ")?; } stringify(f, n)?; if let Some(e) = n.as_element() { for kid in e.traversal_children() { writeln!(f, "")?; fmt_subtree(f, stringify, kid, indent + 1)?; } } Ok(()) } /// The ShadowRoot trait. pub trait TShadowRoot: Sized + Copy + Clone + Debug + PartialEq { /// The concrete node type. type ConcreteNode: TNode; /// Get this ShadowRoot as a node. fn as_node(&self) -> Self::ConcreteNode; /// Get the shadow host that hosts this ShadowRoot. fn host(&self) -> ::ConcreteElement; /// Get the style data for this ShadowRoot. fn style_data<'a>(&self) -> Option<&'a CascadeData> where Self: 'a; /// Get the list of shadow parts for this shadow root. fn parts<'a>(&self) -> &[::ConcreteElement] where Self: 'a, { &[] } /// Get a list of elements with a given ID in this shadow root, sorted by /// tree position. /// /// Can return an error to signal that this list is not available, or also /// return an empty slice. fn elements_with_id<'a>( &self, _id: &AtomIdent, ) -> Result<&'a [::ConcreteElement], ()> where Self: 'a, { Err(()) } /// Get the implicit scope for a stylesheet in given index. fn implicit_scope_for_sheet(&self, _sheet_index: usize) -> Option { None } } /// The element trait, the main abstraction the style crate acts over. pub trait TElement: Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + SelectorsElement + AttributeProvider { /// The concrete node type. type ConcreteNode: TNode; /// A concrete children iterator type in order to iterate over the `Node`s. /// /// TODO(emilio): We should eventually replace this with the `impl Trait` /// syntax. type TraversalChildrenIterator: Iterator; /// Get this element as a node. fn as_node(&self) -> Self::ConcreteNode; /// A debug-only check that the device's owner doc matches the actual doc /// we're the root of. /// /// Otherwise we may set document-level state incorrectly, like the root /// font-size used for rem units. fn owner_doc_matches_for_testing(&self, _: &Device) -> bool { true } /// Whether this element should match user and content rules. /// /// We use this for Native Anonymous Content in Gecko. fn matches_user_and_content_rules(&self) -> bool { true } /// Get this node's parent element from the perspective of a restyle /// traversal. fn traversal_parent(&self) -> Option { self.as_node().traversal_parent() } /// Get this node's children from the perspective of a restyle traversal. fn traversal_children(&self) -> LayoutIterator; /// Returns the parent element we should inherit from. /// /// This is pretty much always the parent element itself, except in the case /// of Gecko's Native Anonymous Content, which uses the traversal parent /// (i.e. the flattened tree parent) and which also may need to find the /// closest non-NAC ancestor. fn inheritance_parent(&self) -> Option { self.parent_element() } /// Execute `f` for each anonymous content child (apart from ::before and /// ::after) whose originating element is `self`. fn each_anonymous_content_child(&self, _f: F) where F: FnMut(Self), { } /// Return whether this element is an element in the HTML namespace. fn is_html_element(&self) -> bool; /// Return whether this element is an element in the MathML namespace. fn is_mathml_element(&self) -> bool; /// Return whether this element is an element in the SVG namespace. fn is_svg_element(&self) -> bool; /// Return whether this element is an element in the XUL namespace. fn is_xul_element(&self) -> bool { false } /// Returns the bloom filter for this element's subtree, used for fast /// querySelector optimization by allowing subtrees to be skipped. /// Each element's filter includes hashes for all of it's class names and /// attribute names (not values), along with the names for all descendent /// elements. /// /// The default implementation returns all bits set, meaning the bloom filter /// never filters anything. fn subtree_bloom_filter(&self) -> u64 { u64::MAX } /// Check if this element's subtree may contain elements with the given bloom hash. fn bloom_may_have_hash(&self, bloom_hash: u64) -> bool { let bloom = self.subtree_bloom_filter(); (bloom & bloom_hash) == bloom_hash } /// Convert a 32-bit atom hash to a bloom filter value using k=2 hash functions. /// This must match the C++ implementation of HashForBloomFilter in Element.cpp fn hash_for_bloom_filter(hash: u32) -> u64 { // On 32-bit platforms, we have 31 bits available + 1 tag bit. // On 64-bit platforms, we have 63 bits available + 1 tag bit. #[cfg(target_pointer_width = "32")] const BLOOM_BITS: u32 = 31; #[cfg(target_pointer_width = "64")] const BLOOM_BITS: u32 = 63; let mut filter = 1u64; filter |= 1u64 << (1 + (hash % BLOOM_BITS)); filter |= 1u64 << (1 + ((hash >> 6) % BLOOM_BITS)); filter } /// Return the list of slotted nodes of this node. fn slotted_nodes(&self) -> &[Self::ConcreteNode] { &[] } /// Get this element's style attribute. fn style_attribute(&self) -> Option>>; /// Unset the style attribute's dirty bit. /// Servo doesn't need to manage ditry bit for style attribute. fn unset_dirty_style_attribute(&self) {} /// Get this element's SMIL override declarations. fn smil_override(&self) -> Option>> { None } /// Get the combined animation and transition rules. /// /// FIXME(emilio): Is this really useful? fn animation_declarations(&self, context: &SharedStyleContext) -> AnimationDeclarations { if !self.may_have_animations() { return Default::default(); } AnimationDeclarations { animations: self.animation_rule(context), transitions: self.transition_rule(context), } } /// Get this element's animation rule. fn animation_rule( &self, _: &SharedStyleContext, ) -> Option>>; /// Get this element's transition rule. fn transition_rule( &self, context: &SharedStyleContext, ) -> Option>>; /// Get this element's state, for non-tree-structural pseudos. fn state(&self) -> ElementState; /// Returns whether this element has a `part` attribute. fn has_part_attr(&self) -> bool; /// Returns whether this element exports any part from its shadow tree. fn exports_any_part(&self) -> bool; /// The ID for this element. fn id(&self) -> Option<&WeakAtom>; /// Internal iterator for the classes of this element. fn each_class(&self, callback: F) where F: FnMut(&AtomIdent); /// Internal iterator for the classes of this element. fn each_custom_state(&self, callback: F) where F: FnMut(&AtomIdent); /// Internal iterator for the part names of this element. fn each_part(&self, _callback: F) where F: FnMut(&AtomIdent), { } /// Internal iterator for the attribute names of this element. fn each_attr_name(&self, callback: F) where F: FnMut(&LocalName); /// Internal iterator for the part names that this element exports for a /// given part name. fn each_exported_part(&self, _name: &AtomIdent, _callback: F) where F: FnMut(&AtomIdent), { } /// Whether a given element may generate a pseudo-element. /// /// This is useful to avoid computing, for example, pseudo styles for /// `::-first-line` or `::-first-letter`, when we know it won't affect us. /// /// TODO(emilio, bz): actually implement the logic for it. fn may_generate_pseudo(&self, pseudo: &PseudoElement, _primary_style: &ComputedValues) -> bool { // ::before/::after are always supported for now, though we could try to // optimize out leaf elements. // ::first-letter and ::first-line are only supported for block-inside // things, and only in Gecko, not Servo. Unfortunately, Gecko has // block-inside things that might have any computed display value due to // things like fieldsets, legends, etc. Need to figure out how this // should work. debug_assert!( pseudo.is_eager(), "Someone called may_generate_pseudo with a non-eager pseudo." ); true } /// Returns true if this element may have a descendant needing style processing. /// /// Note that we cannot guarantee the existence of such an element, because /// it may have been removed from the DOM between marking it for restyle and /// the actual restyle traversal. fn has_dirty_descendants(&self) -> bool; /// Returns whether state or attributes that may change style have changed /// on the element, and thus whether the element has been snapshotted to do /// restyle hint computation. fn has_snapshot(&self) -> bool; /// Returns whether the current snapshot if present has been handled. fn handled_snapshot(&self) -> bool; /// Flags this element as having handled already its snapshot. unsafe fn set_handled_snapshot(&self); /// Returns whether the element's styles are up-to-date after traversal /// (i.e. in post traversal). fn has_current_styles(&self, data: &ElementData) -> bool { if self.has_snapshot() && !self.handled_snapshot() { return false; } data.has_styles() && // TODO(hiro): When an animating element moved into subtree of // contenteditable element, there remains animation restyle hints in // post traversal. It's generally harmless since the hints will be // processed in a next styling but ideally it should be processed soon. // // Without this, we get failures in: // layout/style/crashtests/1383319.html // layout/style/crashtests/1383001.html // // https://bugzilla.mozilla.org/show_bug.cgi?id=1389675 tracks fixing // this. !data.hint.has_non_animation_invalidations() } /// Flag that this element has a descendant for style processing. /// /// Only safe to call with exclusive access to the element. unsafe fn set_dirty_descendants(&self); /// Flag that this element has no descendant for style processing. /// /// Only safe to call with exclusive access to the element. unsafe fn unset_dirty_descendants(&self); /// Similar to the dirty_descendants but for representing a descendant of /// the element needs to be updated in animation-only traversal. fn has_animation_only_dirty_descendants(&self) -> bool { false } /// Flag that this element has a descendant for animation-only restyle /// processing. /// /// Only safe to call with exclusive access to the element. unsafe fn set_animation_only_dirty_descendants(&self) {} /// Flag that this element has no descendant for animation-only restyle processing. /// /// Only safe to call with exclusive access to the element. unsafe fn unset_animation_only_dirty_descendants(&self) {} /// Clear all bits related describing the dirtiness of descendants. /// /// In Gecko, this corresponds to the regular dirty descendants bit, the /// animation-only dirty descendants bit, and the lazy frame construction /// descendants bit. unsafe fn clear_descendant_bits(&self) { self.unset_dirty_descendants(); } /// Returns true if this element is a visited link. /// /// Servo doesn't support visited styles yet. fn is_visited_link(&self) -> bool { false } /// Returns the pseudo-element implemented by this element, if any. /// /// Gecko traverses pseudo-elements during the style traversal, and we need /// to know this so we can properly grab the pseudo-element style from the /// parent element. /// /// Note that we still need to compute the pseudo-elements before-hand, /// given otherwise we don't know if we need to create an element or not. fn implemented_pseudo_element(&self) -> Option { None } /// Atomically stores the number of children of this node that we will /// need to process during bottom-up traversal. fn store_children_to_process(&self, n: isize); /// Atomically notes that a child has been processed during bottom-up /// traversal. Returns the number of children left to process. fn did_process_child(&self) -> isize; /// Gets a reference to the ElementData container, or creates one. /// /// Unsafe because it can race to allocate and leak if not used with /// exclusive access to the element. unsafe fn ensure_data(&self) -> ElementDataMut<'_>; /// Clears the element data reference, if any. /// /// Unsafe following the same reasoning as ensure_data. unsafe fn clear_data(&self); /// Whether there is an ElementData container. fn has_data(&self) -> bool; /// Immutably borrows the ElementData. fn borrow_data(&self) -> Option>; /// Mutably borrows the ElementData. fn mutate_data(&self) -> Option>; /// Whether we should skip any root- or item-based display property /// blockification on this element. (This function exists so that Gecko /// native anonymous content can opt out of this style fixup.) fn skip_item_display_fixup(&self) -> bool; /// In Gecko, element has a flag that represents the element may have /// any type of animations or not to bail out animation stuff early. /// Whereas Servo doesn't have such flag. fn may_have_animations(&self) -> bool; /// Creates a task to update various animation state on a given (pseudo-)element. #[cfg(feature = "gecko")] fn update_animations( &self, before_change_style: Option>, tasks: UpdateAnimationsTasks, ); /// Returns true if the element has relevant animations. Relevant /// animations are those animations that are affecting the element's style /// or are scheduled to do so in the future. fn has_animations(&self, context: &SharedStyleContext) -> bool; /// Returns true if the element has a CSS animation. The `context` and `pseudo_element` /// arguments are only used by Servo, since it stores animations globally and pseudo-elements /// are not in the DOM. fn has_css_animations( &self, context: &SharedStyleContext, pseudo_element: Option, ) -> bool; /// Returns true if the element has a CSS transition (including running transitions and /// completed transitions). The `context` and `pseudo_element` arguments are only used /// by Servo, since it stores animations globally and pseudo-elements are not in the DOM. fn has_css_transitions( &self, context: &SharedStyleContext, pseudo_element: Option, ) -> bool; /// Returns true if the element has animation restyle hints. fn has_animation_restyle_hints(&self) -> bool { let data = match self.borrow_data() { Some(d) => d, None => return false, }; return data.hint.has_animation_hint(); } /// Called when a highlight pseudo-element (::selection, ::highlight, /// ::target-text) style is invalidated. These pseudos need explicit repaint /// triggering since their styles are resolved lazily during painting. fn note_highlight_pseudo_style_invalidated(&self) {} /// The shadow root this element is a host of. fn shadow_root(&self) -> Option<::ConcreteShadowRoot>; /// The shadow root which roots the subtree this element is contained in. fn containing_shadow(&self) -> Option<::ConcreteShadowRoot>; /// Return the element which we can use to look up rules in the selector /// maps. /// /// This is always the element itself, except in the case where we are an /// element-backed pseudo-element, in which case we return the originating /// element. fn rule_hash_target(&self) -> Self { let mut cur = *self; while cur.is_pseudo_element() { cur = cur .pseudo_element_originating_element() .expect("Trying to collect rules for a detached pseudo-element") } cur } /// Executes the callback for each applicable style rule data which isn't /// the main document's data (which stores UA / author rules). /// /// The element passed to the callback is the containing shadow host for the /// data if it comes from Shadow DOM. /// /// Returns whether normal document author rules should apply. /// /// TODO(emilio): We could separate the invalidation data for elements /// matching in other scopes to avoid over-invalidation. fn each_applicable_non_document_style_rule_data<'a, F>(&self, mut f: F) -> bool where Self: 'a, F: FnMut(&'a CascadeData, Self), { use crate::rule_collector::containing_shadow_ignoring_svg_use; let target = self.rule_hash_target(); let matches_user_and_content_rules = target.matches_user_and_content_rules(); let mut doc_rules_apply = matches_user_and_content_rules; // Use the same rules to look for the containing host as we do for rule // collection. if let Some(shadow) = containing_shadow_ignoring_svg_use(target) { doc_rules_apply = false; if let Some(data) = shadow.style_data() { f(data, shadow.host()); } } if let Some(shadow) = target.shadow_root() { if let Some(data) = shadow.style_data() { f(data, shadow.host()); } } let mut current = target.assigned_slot(); while let Some(slot) = current { // Slots can only have assigned nodes when in a shadow tree. let shadow = slot.containing_shadow().unwrap(); if let Some(data) = shadow.style_data() { if data.any_slotted_rule() { f(data, shadow.host()); } } current = slot.assigned_slot(); } if target.has_part_attr() { if let Some(mut inner_shadow) = target.containing_shadow() { loop { let inner_shadow_host = inner_shadow.host(); match inner_shadow_host.containing_shadow() { Some(shadow) => { if let Some(data) = shadow.style_data() { if data.any_part_rule() { f(data, shadow.host()) } } // TODO: Could be more granular. if !inner_shadow_host.exports_any_part() { break; } inner_shadow = shadow; }, None => { // TODO(emilio): Should probably distinguish with // MatchesDocumentRules::{No,Yes,IfPart} or something so that we could // skip some work. doc_rules_apply = matches_user_and_content_rules; break; }, } } } } doc_rules_apply } /// Returns true if one of the transitions needs to be updated on this element. We check all /// the transition properties to make sure that updating transitions is necessary. /// This method should only be called if might_needs_transitions_update returns true when /// passed the same parameters. #[cfg(feature = "gecko")] fn needs_transitions_update( &self, before_change_style: &ComputedValues, after_change_style: &ComputedValues, ) -> bool; /// Returns the value of the `xml:lang=""` attribute (or, if appropriate, /// the `lang=""` attribute) on this element. fn lang_attr(&self) -> Option; /// Returns whether this element's language matches the language tag /// `value`. If `override_lang` is not `None`, it specifies the value /// of the `xml:lang=""` or `lang=""` attribute to use in place of /// looking at the element and its ancestors. (This argument is used /// to implement matching of `:lang()` against snapshots.) fn match_element_lang(&self, override_lang: Option>, value: &Lang) -> bool; /// Returns whether this element is the main body element of the HTML /// document it is on. fn is_html_document_body_element(&self) -> bool; /// Generate the proper applicable declarations due to presentational hints, /// and insert them into `hints`. fn synthesize_presentational_hints_for_legacy_attributes( &self, visited_handling: VisitedHandlingMode, hints: &mut V, ) where V: Push; /// Generate the proper applicable declarations due to view transition dynamic rules, and /// insert them into `rules`. /// https://drafts.csswg.org/css-view-transitions-1/#document-dynamic-view-transition-style-sheet fn synthesize_view_transition_dynamic_rules(&self, _rules: &mut V) where V: Push, { } /// Returns element's local name. fn local_name(&self) -> &::BorrowedLocalName; /// Returns element's namespace. fn namespace(&self) -> &::BorrowedNamespaceUrl; /// Returns the size of the element to be used in container size queries. /// This will usually be the size of the content area of the primary box, /// but can be None if there is no box or if some axis lacks size containment. fn query_container_size( &self, display: &Display, ) -> euclid::default::Size2D>; /// Returns true if the element has all of specified selector flags. fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool; /// Returns the search direction for relative selector invalidation, if it is on the search path. fn relative_selector_search_direction(&self) -> ElementSelectorFlags; /// Returns the implicit scope root for given sheet index and host. fn implicit_scope_for_sheet_in_shadow_root( _opaque_host: OpaqueElement, _sheet_index: usize, ) -> Option { None } /// Compute the damage incurred by the change from the `_old` to `_new`. fn compute_layout_damage(_old: &ComputedValues, _new: &ComputedValues) -> RestyleDamage { Default::default() } } /// The attribute provider trait pub trait AttributeProvider { /// Return the value of the given custom attibute if it exists. fn get_attr(&self, attr: &LocalName, namespace: &Namespace) -> Option; } /// A set of the attributes used to compute a style that uses `attr()` pub type AttributeReferences = Option>>>; /// A data structure to keep track of the names queried from a provider. pub struct AttributeTracker<'a> { /// The element that queries for attributes. pub provider: &'a dyn AttributeProvider, /// The set of attributes we have queried. pub references: AttributeReferences, } impl<'a> AttributeTracker<'a> { /// Construct a new attribute tracker trivially. pub fn new(provider: &'a dyn AttributeProvider) -> Self { Self { provider, references: None, } } /// Consstruct a new dummy attribute tracker pub fn new_dummy() -> Self { Self { provider: &DummyAttributeProvider {}, references: None, } } /// Extract the queried references and consume self pub fn finalize(self) -> AttributeReferences { self.references } /// Query the value and save the name of the attribtue. pub fn query(&mut self, name: &LocalName, namespace: &Namespace) -> Option { // We need to save namespaces in case we are thinking of sharing this element's // style with another. // i.e if elment a has ns1::attr="blue" // and element b has ns2::attr="blue" // a and b can only share style if ns1 and ns2 resolve to the same namespace. self.references .get_or_insert_default() .entry(name.clone()) .or_default() .push(namespace.clone()); self.provider.get_attr(name, namespace) } } /// A dummy AttributeProvider that returns none to any attribute query. #[derive(Clone, Debug, PartialEq)] struct DummyAttributeProvider; impl AttributeProvider for DummyAttributeProvider { fn get_attr(&self, _attr: &LocalName, _namespace: &Namespace) -> Option { None } } /// TNode and TElement aren't Send because we want to be careful and explicit /// about our parallel traversal. However, there are certain situations /// (including but not limited to the traversal) where we need to send DOM /// objects to other threads. /// /// That's the reason why `SendNode` exists. #[derive(Clone, Debug, PartialEq)] pub struct SendNode(N); unsafe impl Send for SendNode {} impl SendNode { /// Unsafely construct a SendNode. pub unsafe fn new(node: N) -> Self { SendNode(node) } } impl Deref for SendNode { type Target = N; fn deref(&self) -> &N { &self.0 } } /// Same reason as for the existence of SendNode, SendElement does the proper /// things for a given `TElement`. #[derive(Debug, Eq, Hash, PartialEq)] pub struct SendElement(E); unsafe impl Send for SendElement {} impl SendElement { /// Unsafely construct a SendElement. pub unsafe fn new(el: E) -> Self { SendElement(el) } } impl Deref for SendElement { type Target = E; fn deref(&self) -> &E { &self.0 } } ================================================ FILE: style/dom_apis.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Generic implementations of some DOM APIs so they can be shared between Servo //! and Gecko. use crate::context::QuirksMode; use crate::dom::{TDocument, TElement, TNode, TShadowRoot}; use crate::invalidation::element::invalidation_map::Dependency; use crate::invalidation::element::invalidator::{ DescendantInvalidationLists, Invalidation, SiblingTraversalMap, }; use crate::invalidation::element::invalidator::{InvalidationProcessor, InvalidationVector}; use crate::selector_parser::SelectorImpl; use crate::values::AtomIdent; use selectors::attr::CaseSensitivity; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; use selectors::matching::{ self, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, SelectorCaches, }; use selectors::parser::{Combinator, Component, LocalName}; use selectors::{Element, OpaqueElement, SelectorList}; use smallvec::SmallVec; /// pub fn element_matches( element: &E, selector_list: &SelectorList, quirks_mode: QuirksMode, ) -> bool where E: Element, { let mut selector_caches = SelectorCaches::default(); let mut context = MatchingContext::new( MatchingMode::Normal, None, &mut selector_caches, quirks_mode, NeedsSelectorFlags::No, MatchingForInvalidation::No, ); context.scope_element = Some(element.opaque()); context.current_host = element.containing_shadow_host().map(|e| e.opaque()); matching::matches_selector_list(selector_list, element, &mut context) } /// pub fn element_closest( element: E, selector_list: &SelectorList, quirks_mode: QuirksMode, ) -> Option where E: Element, { let mut selector_caches = SelectorCaches::default(); let mut context = MatchingContext::new( MatchingMode::Normal, None, &mut selector_caches, quirks_mode, NeedsSelectorFlags::No, MatchingForInvalidation::No, ); context.scope_element = Some(element.opaque()); context.current_host = element.containing_shadow_host().map(|e| e.opaque()); let mut current = Some(element); while let Some(element) = current.take() { if matching::matches_selector_list(selector_list, &element, &mut context) { return Some(element); } current = element.parent_element(); } return None; } /// A selector query abstraction, in order to be generic over QuerySelector and /// QuerySelectorAll. pub trait SelectorQuery { /// The output of the query. type Output; /// Whether the query should stop after the first element has been matched. fn should_stop_after_first_match() -> bool; /// Append an element matching after the first query. fn append_element(output: &mut Self::Output, element: E); /// Returns true if the output is empty. fn is_empty(output: &Self::Output) -> bool; } /// The result of a querySelectorAll call. pub type QuerySelectorAllResult = SmallVec<[E; 128]>; /// A query for all the elements in a subtree. pub struct QueryAll; impl SelectorQuery for QueryAll { type Output = QuerySelectorAllResult; fn should_stop_after_first_match() -> bool { false } fn append_element(output: &mut Self::Output, element: E) { output.push(element); } fn is_empty(output: &Self::Output) -> bool { output.is_empty() } } /// A query for the first in-tree match of all the elements in a subtree. pub struct QueryFirst; impl SelectorQuery for QueryFirst { type Output = Option; fn should_stop_after_first_match() -> bool { true } fn append_element(output: &mut Self::Output, element: E) { if output.is_none() { *output = Some(element) } } fn is_empty(output: &Self::Output) -> bool { output.is_none() } } struct QuerySelectorProcessor<'a, 'b, E, Q> where E: TElement + 'a, Q: SelectorQuery, Q::Output: 'a, { results: &'a mut Q::Output, matching_context: MatchingContext<'b, E::Impl>, traversal_map: SiblingTraversalMap, dependencies: &'a [Dependency], } impl<'a, 'b, E, Q> InvalidationProcessor<'a, 'b, E> for QuerySelectorProcessor<'a, 'b, E, Q> where E: TElement + 'a, Q: SelectorQuery, Q::Output: 'a, { fn light_tree_only(&self) -> bool { true } fn check_outer_dependency(&mut self, _: &Dependency, _: E, _: Option) -> bool { debug_assert!( false, "How? We should only have parent-less dependencies here!" ); true } fn collect_invalidations( &mut self, element: E, self_invalidations: &mut InvalidationVector<'a>, descendant_invalidations: &mut DescendantInvalidationLists<'a>, _sibling_invalidations: &mut InvalidationVector<'a>, ) -> bool { // TODO(emilio): If the element is not a root element, and // selector_list has any descendant combinator, we need to do extra work // in order to handle properly things like: // //
//
//
//
//
// // b.querySelector('#a div'); // Should return "c". // // For now, assert it's a root element. debug_assert!(element.parent_element().is_none()); let target_vector = if self.matching_context.scope_element.is_some() { &mut descendant_invalidations.dom_descendants } else { self_invalidations }; for dependency in self.dependencies.iter() { target_vector.push(Invalidation::new( dependency, self.matching_context.current_host.clone(), self.matching_context.scope_element.clone(), )) } false } fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> { &mut self.matching_context } fn sibling_traversal_map(&self) -> &SiblingTraversalMap { &self.traversal_map } fn should_process_descendants(&mut self, _: E) -> bool { if Q::should_stop_after_first_match() { return Q::is_empty(&self.results); } true } fn invalidated_self(&mut self, e: E) { Q::append_element(self.results, e); } fn invalidated_sibling(&mut self, e: E, _of: E) { Q::append_element(self.results, e); } fn recursion_limit_exceeded(&mut self, _e: E) {} fn invalidated_descendants(&mut self, _e: E, _child: E) {} } enum Operation { Reject, Accept, RejectSkippingChildren, } impl From for Operation { #[inline(always)] fn from(matches: bool) -> Self { if matches { Operation::Accept } else { Operation::Reject } } } fn collect_all_elements(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F) where E: TElement, Q: SelectorQuery, F: FnMut(E) -> Operation, { let mut iter = root.dom_descendants(); let mut cur = iter.next(); while let Some(node) = cur { let element = match node.as_element() { Some(e) => e, None => { cur = iter.next(); continue; }, }; match filter(element) { // Element matches - add to results and continue traversing its children. Operation::Accept => { Q::append_element(results, element); if Q::should_stop_after_first_match() { return; } }, // Element doesn't match - skip it but continue traversing its children. Operation::Reject => {}, // Element doesn't match and skip entire subtree. Operation::RejectSkippingChildren => { cur = iter.next_skipping_children(); continue; }, } cur = iter.next(); } } /// Returns whether a given element connected to `root` is descendant of `root`. /// /// NOTE(emilio): if root == element, this returns false. fn connected_element_is_descendant_of(element: E, root: E::ConcreteNode) -> bool where E: TElement, { // Optimize for when the root is a document or a shadow root and the element // is connected to that root. if root.as_document().is_some() { debug_assert!(element.as_node().is_in_document(), "Not connected?"); debug_assert_eq!( root, root.owner_doc().as_node(), "Where did this element come from?", ); return true; } if root.as_shadow_root().is_some() { debug_assert_eq!( element.containing_shadow().unwrap().as_node(), root, "Not connected?" ); return true; } let mut current = element.as_node().parent_node(); while let Some(n) = current.take() { if n == root { return true; } current = n.parent_node(); } false } /// Fast path for iterating over every element with a given id in the document /// or shadow root that `root` is connected to. fn fast_connected_elements_with_id<'a, N>( root: N, id: &AtomIdent, case_sensitivity: CaseSensitivity, ) -> Result<&'a [N::ConcreteElement], ()> where N: TNode + 'a, { if case_sensitivity != CaseSensitivity::CaseSensitive { return Err(()); } if root.is_in_document() { return root.owner_doc().elements_with_id(id); } if let Some(shadow) = root.as_shadow_root() { return shadow.elements_with_id(id); } if let Some(shadow) = root.as_element().and_then(|e| e.containing_shadow()) { return shadow.elements_with_id(id); } Err(()) } /// Collects elements with a given id under `root`, that pass `filter`. fn collect_elements_with_id( root: E::ConcreteNode, id: &AtomIdent, results: &mut Q::Output, class_and_id_case_sensitivity: CaseSensitivity, mut filter: F, ) where E: TElement, Q: SelectorQuery, F: FnMut(E) -> bool, { let elements = match fast_connected_elements_with_id(root, id, class_and_id_case_sensitivity) { Ok(elements) => elements, Err(()) => { collect_all_elements::(root, results, |e| { Operation::from(e.has_id(id, class_and_id_case_sensitivity) && filter(e)) }); return; }, }; for element in elements { // If the element is not an actual descendant of the root, even though // it's connected, we don't really care about it. if !connected_element_is_descendant_of(*element, root) { continue; } if !filter(*element) { continue; } Q::append_element(results, *element); if Q::should_stop_after_first_match() { break; } } } fn has_attr(element: E, local_name: &crate::LocalName) -> bool where E: TElement, { let mut found = false; element.each_attr_name(|name| found |= name == local_name); found } #[inline(always)] fn local_name_matches(element: E, local_name: &LocalName) -> bool where E: TElement, { let LocalName { ref name, ref lower_name, } = *local_name; let chosen_name = if name == lower_name || element.is_html_element_in_html_document() { lower_name } else { name }; element.local_name() == &**chosen_name } fn get_attr_name(component: &Component) -> Option<&crate::LocalName> { let (name, name_lower) = match component { Component::AttributeInNoNamespace { ref local_name, .. } => return Some(local_name), Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, .. } => (local_name, local_name_lower), Component::AttributeOther(ref attr) => (&attr.local_name, &attr.local_name_lower), _ => return None, }; if name != name_lower { return None; // TODO: Maybe optimize this? } Some(name) } fn get_id(component: &Component) -> Option<&AtomIdent> { use selectors::attr::AttrSelectorOperator; Some(match component { Component::ID(ref id) => id, Component::AttributeInNoNamespace { ref operator, ref local_name, ref value, .. } => { if *local_name != local_name!("id") { return None; } if *operator != AttrSelectorOperator::Equal { return None; } AtomIdent::cast(&value.0) }, _ => return None, }) } /// Fast paths for querySelector with a single simple selector. fn query_selector_single_query( root: E::ConcreteNode, component: &Component, results: &mut Q::Output, class_and_id_case_sensitivity: CaseSensitivity, ) -> Result<(), ()> where E: TElement, Q: SelectorQuery, { match *component { Component::ExplicitUniversalType => { collect_all_elements::(root, results, |_| Operation::Accept) }, Component::Class(ref class) => { // Bloom filter can only be used when case sensitive. let bloom_hash = if class_and_id_case_sensitivity == CaseSensitivity::CaseSensitive { Some(E::hash_for_bloom_filter(class.0.get_hash())) } else { None }; collect_all_elements::(root, results, |element| { if bloom_hash.is_some_and(|hash| !element.bloom_may_have_hash(hash)) { return Operation::RejectSkippingChildren; } Operation::from(element.has_class(class, class_and_id_case_sensitivity)) }); }, Component::LocalName(ref local_name) => { collect_all_elements::(root, results, |element| { Operation::from(local_name_matches(element, local_name)) }) }, Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, } => { // For HTML elements: C++ hashes lowercase // For XUL/SVG/MathML elements: C++ hashes original case let hash_original = E::hash_for_bloom_filter(local_name.0.get_hash()); let hash_lower = if local_name.0 == local_name_lower.0 { hash_original } else { E::hash_for_bloom_filter(local_name_lower.0.get_hash()) }; collect_all_elements::(root, results, |element| { // Check bloom filter first let bloom_found_hash = if hash_original == hash_lower || !element.as_node().owner_doc().is_html_document() { element.bloom_may_have_hash(hash_original) } else if element.is_html_element_in_html_document() { // HTML elements store lowercase hashes element.bloom_may_have_hash(hash_lower) } else { // Non-HTML elements in HTML documents might have HTML descendants // with lowercase-only hashes, so check both element.bloom_may_have_hash(hash_original) || element.bloom_may_have_hash(hash_lower) }; if !bloom_found_hash { return Operation::RejectSkippingChildren; } Operation::from(element.has_attr_in_no_namespace(matching::select_name( &element, local_name, local_name_lower, ))) }); }, Component::AttributeInNoNamespace { ref local_name, ref value, operator, case_sensitivity, } => { let empty_namespace = selectors::parser::namespace_empty_string::(); let namespace_constraint = NamespaceConstraint::Specific(&empty_namespace); // Only use bloom filter to check for attribute name existence. let bloom_hash = E::hash_for_bloom_filter(local_name.0.get_hash()); collect_all_elements::(root, results, |element| { if !element.bloom_may_have_hash(bloom_hash) { return Operation::RejectSkippingChildren; } Operation::from(element.attr_matches( &namespace_constraint, local_name, &AttrSelectorOperation::WithValue { operator, case_sensitivity: matching::to_unconditional_case_sensitivity( case_sensitivity, &element, ), value, }, )) }); }, ref other => { let id = match get_id(other) { Some(id) => id, // TODO(emilio): More fast paths? None => return Err(()), }; collect_elements_with_id::( root, id, results, class_and_id_case_sensitivity, |_| true, ); }, } Ok(()) } enum SimpleFilter<'a> { Class(&'a AtomIdent), Attr(&'a crate::LocalName), LocalName(&'a LocalName), } /// Fast paths for a given selector query. /// /// When there's only one component, we go directly to /// `query_selector_single_query`, otherwise, we try to optimize by looking just /// at the subtrees rooted at ids in the selector, and otherwise we try to look /// up by class name or local name in the rightmost compound. /// /// FIXME(emilio, nbp): This may very well be a good candidate for code to be /// replaced by HolyJit :) fn query_selector_fast( root: E::ConcreteNode, selector_list: &SelectorList, results: &mut Q::Output, matching_context: &mut MatchingContext, ) -> Result<(), ()> where E: TElement, Q: SelectorQuery, { // We need to return elements in document order, and reordering them // afterwards is kinda silly. if selector_list.len() > 1 { return Err(()); } let selector = &selector_list.slice()[0]; let class_and_id_case_sensitivity = matching_context.classes_and_ids_case_sensitivity(); // Let's just care about the easy cases for now. if selector.len() == 1 { if query_selector_single_query::( root, selector.iter().next().unwrap(), results, class_and_id_case_sensitivity, ) .is_ok() { return Ok(()); } } let mut iter = selector.iter(); let mut combinator: Option = None; // We want to optimize some cases where there's no id involved whatsoever, // like `.foo .bar`, but we don't want to make `#foo .bar` slower because of // that. let mut simple_filter = None; 'selector_loop: loop { debug_assert!(combinator.map_or(true, |c| !c.is_sibling())); 'component_loop: for component in &mut iter { match *component { Component::Class(ref class) => { if combinator.is_none() { simple_filter = Some(SimpleFilter::Class(class)); } }, Component::LocalName(ref local_name) => { if combinator.is_none() { // Prefer to look at class rather than local-name if // both are present. if let Some(SimpleFilter::Class(..)) = simple_filter { continue; } simple_filter = Some(SimpleFilter::LocalName(local_name)); } }, ref other => { if let Some(id) = get_id(other) { if combinator.is_none() { // In the rightmost compound, just find descendants of root that match // the selector list with that id. collect_elements_with_id::( root, id, results, class_and_id_case_sensitivity, |e| { matching::matches_selector_list( selector_list, &e, matching_context, ) }, ); return Ok(()); } let elements = fast_connected_elements_with_id( root, id, class_and_id_case_sensitivity, )?; if elements.is_empty() { return Ok(()); } // Results need to be in document order. Let's not bother // reordering or deduplicating nodes, which we would need to // do if one element with the given id were a descendant of // another element with that given id. if !Q::should_stop_after_first_match() && elements.len() > 1 { continue; } for element in elements { // If the element is not a descendant of the root, then // it may have descendants that match our selector that // _are_ descendants of the root, and other descendants // that match our selector that are _not_. // // So we can't just walk over the element's descendants // and match the selector against all of them, nor can // we skip looking at this element's descendants. // // Give up on trying to optimize based on this id and // keep walking our selector. if !connected_element_is_descendant_of(*element, root) { continue 'component_loop; } query_selector_slow::( element.as_node(), selector_list, results, matching_context, ); if Q::should_stop_after_first_match() && !Q::is_empty(&results) { break; } } return Ok(()); } if combinator.is_none() && simple_filter.is_none() { if let Some(attr_name) = get_attr_name(other) { simple_filter = Some(SimpleFilter::Attr(attr_name)); } } }, } } loop { let next_combinator = match iter.next_sequence() { None => break 'selector_loop, Some(c) => c, }; // We don't want to scan stuff affected by sibling combinators, // given we scan the subtree of elements with a given id (and we // don't want to care about scanning the siblings' subtrees). if next_combinator.is_sibling() { // Advance to the next combinator. for _ in &mut iter {} continue; } combinator = Some(next_combinator); break; } } // We got here without finding any ID or such that we could handle. Try to // use one of the simple filters. let simple_filter = match simple_filter { Some(f) => f, None => return Err(()), }; match simple_filter { SimpleFilter::Class(ref class) => { collect_all_elements::(root, results, |element| { Operation::from( element.has_class(class, class_and_id_case_sensitivity) && matching::matches_selector_list( selector_list, &element, matching_context, ), ) }); }, SimpleFilter::LocalName(ref local_name) => { collect_all_elements::(root, results, |element| { Operation::from( local_name_matches(element, local_name) && matching::matches_selector_list( selector_list, &element, matching_context, ), ) }); }, SimpleFilter::Attr(ref local_name) => { collect_all_elements::(root, results, |element| { Operation::from( has_attr(element, local_name) && matching::matches_selector_list( selector_list, &element, matching_context, ), ) }); }, } Ok(()) } // Slow path for a given selector query. fn query_selector_slow( root: E::ConcreteNode, selector_list: &SelectorList, results: &mut Q::Output, matching_context: &mut MatchingContext, ) where E: TElement, Q: SelectorQuery, { collect_all_elements::(root, results, |element| { Operation::from(matching::matches_selector_list( selector_list, &element, matching_context, )) }); } /// Whether the invalidation machinery should be used for this query. #[derive(PartialEq)] pub enum MayUseInvalidation { /// We may use it if we deem it useful. Yes, /// Don't use it. No, } /// pub fn query_selector( root: E::ConcreteNode, selector_list: &SelectorList, results: &mut Q::Output, may_use_invalidation: MayUseInvalidation, ) where E: TElement, Q: SelectorQuery, { use crate::invalidation::element::invalidator::TreeStyleInvalidator; let mut selector_caches = SelectorCaches::default(); let quirks_mode = root.owner_doc().quirks_mode(); let mut matching_context = MatchingContext::new( MatchingMode::Normal, None, &mut selector_caches, quirks_mode, NeedsSelectorFlags::No, MatchingForInvalidation::No, ); let root_element = root.as_element(); matching_context.scope_element = root_element.map(|e| e.opaque()); matching_context.current_host = match root_element { Some(root) => root.containing_shadow_host().map(|host| host.opaque()), None => root.as_shadow_root().map(|root| root.host().opaque()), }; let fast_result = query_selector_fast::(root, selector_list, results, &mut matching_context); if fast_result.is_ok() { return; } // Slow path: Use the invalidation machinery if we're a root, and tree // traversal otherwise. // // See the comment in collect_invalidations to see why only if we're a root. // // The invalidation mechanism is only useful in presence of combinators. // // We could do that check properly here, though checking the length of the // selectors is a good heuristic. // // A selector with a combinator needs to have a length of at least 3: A // simple selector, a combinator, and another simple selector. let invalidation_may_be_useful = may_use_invalidation == MayUseInvalidation::Yes && selector_list.slice().iter().any(|s| s.len() > 2); if root_element.is_some() || !invalidation_may_be_useful { query_selector_slow::(root, selector_list, results, &mut matching_context); } else { let dependencies = selector_list .slice() .iter() .map(|selector| Dependency::for_full_selector_invalidation(selector.clone())) .collect::>(); let mut processor = QuerySelectorProcessor:: { results, matching_context, traversal_map: SiblingTraversalMap::default(), dependencies: &dependencies, }; for node in root.dom_children() { if let Some(e) = node.as_element() { TreeStyleInvalidator::new(e, /* stack_limit_checker = */ None, &mut processor) .invalidate(); } } } } ================================================ FILE: style/driver.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Implements traversal over the DOM tree. The traversal starts in sequential //! mode, and optionally parallelizes as it discovers work. #![deny(missing_docs)] use crate::context::{PerThreadTraversalStatistics, StyleContext}; use crate::context::{ThreadLocalStyleContext, TraversalStatistics}; use crate::dom::{SendNode, TElement, TNode}; use crate::parallel; use crate::scoped_tls::ScopedTLS; use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken}; use std::collections::VecDeque; use std::time::Instant; #[cfg(feature = "servo")] fn should_report_statistics() -> bool { false } #[cfg(feature = "gecko")] fn should_report_statistics() -> bool { unsafe { !crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton.is_null() } } #[cfg(feature = "servo")] fn report_statistics(_stats: &PerThreadTraversalStatistics) { unreachable!("Servo never report stats"); } #[cfg(feature = "gecko")] fn report_statistics(stats: &PerThreadTraversalStatistics) { // This should only be called in the main thread, or it may be racy // to update the statistics in a global variable. unsafe { debug_assert!(crate::gecko_bindings::bindings::Gecko_IsMainThread()); let gecko_stats = crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton; (*gecko_stats).mElementsTraversed += stats.elements_traversed; (*gecko_stats).mElementsStyled += stats.elements_styled; (*gecko_stats).mElementsMatched += stats.elements_matched; (*gecko_stats).mStylesShared += stats.styles_shared; (*gecko_stats).mStylesReused += stats.styles_reused; } } fn with_pool_in_place_scope<'scope>( work_unit_max: usize, pool: Option<&rayon::ThreadPool>, closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) + Send + 'scope, ) { if work_unit_max == 0 || pool.is_none() { closure(None); } else { let pool = pool.unwrap(); pool.in_place_scope_fifo(|scope| { #[cfg(feature = "gecko")] debug_assert_eq!( pool.current_thread_index(), Some(0), "Main thread should be the first thread" ); if cfg!(feature = "gecko") || pool.current_thread_index().is_some() { closure(Some(scope)); } else { scope.spawn_fifo(|scope| closure(Some(scope))); } }); } } /// See documentation of the pref for performance characteristics. fn work_unit_max() -> usize { static_prefs::pref!("layout.css.stylo-work-unit-size") as usize } /// Do a DOM traversal for top-down and (optionally) bottom-up processing, generic over `D`. /// /// We use an adaptive traversal strategy. We start out with simple sequential processing, until we /// arrive at a wide enough level in the DOM that the parallel traversal would parallelize it. /// If a thread pool is provided, we then transfer control over to the parallel traversal. /// /// Returns true if the traversal was parallel, and also returns the statistics object containing /// information on nodes traversed (on nightly only). Not all of its fields will be initialized /// since we don't call finish(). pub fn traverse_dom( traversal: &D, token: PreTraverseToken, pool: Option<&rayon::ThreadPool>, ) -> E where E: TElement, D: DomTraversal, { let root = token .traversal_root() .expect("Should've ensured we needed to traverse"); let report_stats = should_report_statistics(); let dump_stats = traversal.shared_context().options.dump_style_statistics; let start_time = if dump_stats { Some(Instant::now()) } else { None }; // Declare the main-thread context, as well as the worker-thread contexts, // which we may or may not instantiate. It's important to declare the worker- // thread contexts first, so that they get dropped second. This matters because: // * ThreadLocalContexts borrow AtomicRefCells in TLS. // * Dropping a ThreadLocalContext can run SequentialTasks. // * Sequential tasks may call into functions like // Servo_StyleSet_GetBaseComputedValuesForElement, which instantiate a // ThreadLocalStyleContext on the main thread. If the main thread // ThreadLocalStyleContext has not released its TLS borrow by that point, // we'll panic on double-borrow. let mut scoped_tls = ScopedTLS::>::new(pool); // Process the nodes breadth-first. This helps keep similar traversal characteristics for the // style sharing cache. let work_unit_max = work_unit_max(); let send_root = unsafe { SendNode::new(root.as_node()) }; with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| { let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context); let mut context = StyleContext { shared: traversal.shared_context(), thread_local: &mut tlc, }; let mut discovered = VecDeque::with_capacity(work_unit_max * 2); let current_dom_depth = send_root.depth(); let opaque_root = send_root.opaque(); discovered.push_back(send_root); parallel::style_trees( &mut context, discovered, opaque_root, work_unit_max, PerLevelTraversalData { current_dom_depth }, maybe_scope, traversal, &scoped_tls, ); }); // Collect statistics from thread-locals if requested. if dump_stats || report_stats { let mut aggregate = PerThreadTraversalStatistics::default(); for slot in scoped_tls.slots() { if let Some(cx) = slot.get_mut() { aggregate += cx.statistics.clone(); } } if report_stats { report_statistics(&aggregate); } // dump statistics to stdout if requested if dump_stats { let parallel = pool.is_some(); let stats = TraversalStatistics::new(aggregate, traversal, parallel, start_time.unwrap()); if stats.is_large { println!("{}", stats); } } } root } ================================================ FILE: style/error_reporting.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Types used to report parsing errors. #![deny(missing_docs)] use crate::selector_parser::SelectorImpl; use crate::stylesheets::UrlExtraData; use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token}; use selectors::parser::{Combinator, Component, RelativeSelector, Selector}; use selectors::visitor::{SelectorListKind, SelectorVisitor}; use selectors::SelectorList; use std::fmt; use style_traits::ParseError; /// Errors that can be encountered while parsing CSS. #[derive(Debug)] pub enum ContextualParseError<'a> { /// A property declaration was not recognized. UnsupportedPropertyDeclaration(&'a str, ParseError<'a>, &'a [SelectorList]), /// A property descriptor was not recognized. UnsupportedPropertyDescriptor(&'a str, ParseError<'a>), /// A font face descriptor was not recognized. UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>), /// A font feature values descriptor was not recognized. UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>), /// A font palette values descriptor was not recognized. UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>), /// A keyframe rule was not valid. InvalidKeyframeRule(&'a str, ParseError<'a>), /// A font feature values rule was not valid. InvalidFontFeatureValuesRule(&'a str, ParseError<'a>), /// A rule was invalid for some reason. InvalidRule(&'a str, ParseError<'a>), /// A rule was not recognized. UnsupportedRule(&'a str, ParseError<'a>), /// A viewport descriptor declaration was not recognized. UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>), /// A counter style descriptor declaration was not recognized. UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>), /// A counter style rule had no symbols. InvalidCounterStyleWithoutSymbols(String), /// A counter style rule had less than two symbols. InvalidCounterStyleNotEnoughSymbols(String), /// A counter style rule did not have additive-symbols. InvalidCounterStyleWithoutAdditiveSymbols, /// A counter style rule had extends with symbols. InvalidCounterStyleExtendsWithSymbols, /// A counter style rule had extends with additive-symbols. InvalidCounterStyleExtendsWithAdditiveSymbols, /// A media rule was invalid for some reason. InvalidMediaRule(&'a str, ParseError<'a>), /// A value was not recognized. UnsupportedValue(&'a str, ParseError<'a>), /// A never-matching `:host` selector was found. NeverMatchingHostSelector(String), /// A view-transition declaration was not recognized. UnsupportedViewTransitionDescriptor(&'a str, ParseError<'a>), } impl<'a> fmt::Display for ContextualParseError<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result { match *t { Token::Ident(ref i) => write!(f, "identifier {}", i), Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw), Token::Hash(ref h) => write!(f, "hash #{}", h), Token::IDHash(ref h) => write!(f, "id selector #{}", h), Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s), Token::UnquotedUrl(ref u) => write!(f, "url {}", u), Token::Delim(ref d) => write!(f, "delimiter {}", d), Token::Number { int_value: Some(i), .. } => write!(f, "number {}", i), Token::Number { value, .. } => write!(f, "number {}", value), Token::Percentage { int_value: Some(i), .. } => write!(f, "percentage {}", i), Token::Percentage { unit_value, .. } => { write!(f, "percentage {}", unit_value * 100.) }, Token::Dimension { value, ref unit, .. } => write!(f, "dimension {}{}", value, unit), Token::WhiteSpace(_) => write!(f, "whitespace"), Token::Comment(_) => write!(f, "comment"), Token::Colon => write!(f, "colon (:)"), Token::Semicolon => write!(f, "semicolon (;)"), Token::Comma => write!(f, "comma (,)"), Token::IncludeMatch => write!(f, "include match (~=)"), Token::DashMatch => write!(f, "dash match (|=)"), Token::PrefixMatch => write!(f, "prefix match (^=)"), Token::SuffixMatch => write!(f, "suffix match ($=)"), Token::SubstringMatch => write!(f, "substring match (*=)"), Token::CDO => write!(f, "CDO ()"), Token::Function(ref name) => write!(f, "function {}", name), Token::ParenthesisBlock => write!(f, "parenthesis ("), Token::SquareBracketBlock => write!(f, "square bracket ["), Token::CurlyBracketBlock => write!(f, "curly bracket {{"), Token::BadUrl(ref _u) => write!(f, "bad url parse error"), Token::BadString(ref _s) => write!(f, "bad string parse error"), Token::CloseParenthesis => write!(f, "unmatched close parenthesis"), Token::CloseSquareBracket => write!(f, "unmatched close square bracket"), Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"), } } fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result { match err.kind { ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => { write!(f, "found unexpected ")?; token_to_str(t, f) }, ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => { write!(f, "unexpected end of input") }, ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => { write!(f, "@ rule invalid: {}", i) }, ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => { write!(f, "@ rule invalid") }, ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => { write!(f, "qualified rule invalid") }, ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err), } } match *self { ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => { write!(f, "Unsupported property declaration: '{}', ", decl)?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => { write!( f, "Unsupported @property descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => { write!( f, "Unsupported @font-face descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => { write!( f, "Unsupported @font-feature-values descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => { write!( f, "Unsupported @font-palette-values descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::InvalidKeyframeRule(rule, ref err) => { write!(f, "Invalid keyframe rule: '{}', ", rule)?; parse_error_to_str(err, f) }, ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => { write!(f, "Invalid font feature value rule: '{}', ", rule)?; parse_error_to_str(err, f) }, ContextualParseError::InvalidRule(rule, ref err) => { write!(f, "Invalid rule: '{}', ", rule)?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedRule(rule, ref err) => { write!(f, "Unsupported rule: '{}', ", rule)?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => { write!( f, "Unsupported @viewport descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => { write!( f, "Unsupported @counter-style descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!( f, "Invalid @counter-style rule: 'system: {}' without 'symbols'", system ), ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!( f, "Invalid @counter-style rule: 'system: {}' less than two 'symbols'", system ), ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!( f, "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'" ), ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!( f, "Invalid @counter-style rule: 'system: extends …' with 'symbols'" ), ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!( f, "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'" ), ContextualParseError::InvalidMediaRule(media_rule, ref err) => { write!(f, "Invalid media rule: {}, ", media_rule)?; parse_error_to_str(err, f) }, ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f), ContextualParseError::NeverMatchingHostSelector(ref selector) => { write!(f, ":host selector is not featureless: {}", selector) }, ContextualParseError::UnsupportedViewTransitionDescriptor(decl, ref err) => { write!( f, "Unsupported @view-transition descriptor declaration: '{}', ", decl )?; parse_error_to_str(err, f) }, } } } /// A generic trait for an error reporter. pub trait ParseErrorReporter { /// Called when the style engine detects an error. /// /// Returns the current input being parsed, the source location it was /// reported from, and a message. fn report_error( &self, url: &UrlExtraData, location: SourceLocation, error: ContextualParseError, ); } /// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log) /// at `info` level. /// /// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info` /// environment variable. /// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).) #[cfg(feature = "servo")] pub struct RustLogReporter; #[cfg(feature = "servo")] impl ParseErrorReporter for RustLogReporter { fn report_error( &self, url: &UrlExtraData, location: SourceLocation, error: ContextualParseError, ) { if log_enabled!(log::Level::Info) { info!( "Url:\t{}\n{}:{} {}", url.as_str(), location.line, location.column, error ) } } } /// Any warning a selector may generate. /// TODO(dshin): Bug 1860634 - Merge with never matching host selector warning, which is part of the rule parser. #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SelectorWarningKind { /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`. /// May cause expensive invalidations for every element inserted and/or removed. UnconstraintedRelativeSelector, /// `:scope` can have 3 meanings, but in all cases, the relationship is defined strictly by an ancestor-descendant /// relationship. This means that any presence of sibling selectors to its right would make it never match. SiblingCombinatorAfterScopeSelector, } impl SelectorWarningKind { /// Get all warnings for this selector. pub fn from_selector(selector: &Selector) -> Vec { let mut result = vec![]; if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) { result.push(SelectorWarningKind::UnconstraintedRelativeSelector); } if SiblingCombinatorAfterScopeSelectorVisitor::has_warning(selector) { result.push(SelectorWarningKind::SiblingCombinatorAfterScopeSelector); } result } } /// Per-compound state for finding unconstrained relative selectors. struct PerCompoundState { /// Is there a relative selector in this compound? relative_selector_found: bool, /// Is this compound constrained in any way? constrained: bool, /// Nested below, or inside relative selector? in_relative_selector: bool, } impl PerCompoundState { fn new(in_relative_selector: bool) -> Self { Self { relative_selector_found: false, constrained: false, in_relative_selector, } } } /// Visitor to check if there's any unconstrained relative selector. struct UnconstrainedRelativeSelectorVisitor { compound_state: PerCompoundState, } impl UnconstrainedRelativeSelectorVisitor { fn new(in_relative_selector: bool) -> Self { Self { compound_state: PerCompoundState::new(in_relative_selector), } } fn has_warning( selector: &Selector, offset: usize, in_relative_selector: bool, ) -> bool { let relative_selector = matches!( selector.iter_raw_parse_order_from(0).next().unwrap(), Component::RelativeSelectorAnchor ); debug_assert!( !relative_selector || offset == 0, "Checking relative selector from non-rightmost?" ); let mut visitor = Self::new(in_relative_selector); let mut iter = if relative_selector { selector.iter_skip_relative_selector_anchor() } else { selector.iter_from(offset) }; loop { visitor.compound_state = PerCompoundState::new(in_relative_selector); for s in &mut iter { s.visit(&mut visitor); } if (visitor.compound_state.relative_selector_found || visitor.compound_state.in_relative_selector) && !visitor.compound_state.constrained { return true; } if iter.next_sequence().is_none() { break; } } false } } impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor { type Impl = SelectorImpl; fn visit_simple_selector(&mut self, c: &Component) -> bool { match c { // Deferred to visit_selector_list Component::Is(..) | Component::Where(..) | Component::Negation(..) | Component::Has(..) => (), Component::ExplicitUniversalType => (), _ => self.compound_state.constrained |= true, }; true } fn visit_selector_list( &mut self, _list_kind: SelectorListKind, list: &[Selector], ) -> bool { let mut all_constrained = true; for s in list { let mut offset = 0; // First, check the rightmost compound for constraint at this level. if !self.compound_state.in_relative_selector { let mut nested = Self::new(false); let mut iter = s.iter(); loop { for c in &mut iter { c.visit(&mut nested); offset += 1; } let c = iter.next_sequence(); offset += 1; if c.map_or(true, |c| !c.is_pseudo_element()) { break; } } // Every single selector in the list must be constrained. all_constrained &= nested.compound_state.constrained; } if offset >= s.len() { continue; } // Then, recurse in to check at the deeper level. if Self::has_warning(s, offset, self.compound_state.in_relative_selector) { self.compound_state.constrained = false; if !self.compound_state.in_relative_selector { self.compound_state.relative_selector_found = true; } return false; } } self.compound_state.constrained |= all_constrained; true } fn visit_relative_selector_list(&mut self, list: &[RelativeSelector]) -> bool { debug_assert!( !self.compound_state.in_relative_selector, "Nested relative selector" ); self.compound_state.relative_selector_found = true; for rs in list { // If the inside is unconstrained, we are unconstrained no matter what. if Self::has_warning(&rs.selector, 0, true) { self.compound_state.constrained = false; return false; } } true } } struct SiblingCombinatorAfterScopeSelectorVisitor { right_combinator_is_sibling: bool, found: bool, } impl SiblingCombinatorAfterScopeSelectorVisitor { fn new(right_combinator_is_sibling: bool) -> Self { Self { right_combinator_is_sibling, found: false, } } fn has_warning(selector: &Selector) -> bool { if !selector.has_scope_selector() { return false; } let visitor = SiblingCombinatorAfterScopeSelectorVisitor::new(false); visitor.find_never_matching_scope_selector(selector) } fn find_never_matching_scope_selector(mut self, selector: &Selector) -> bool { selector.visit(&mut self); self.found } } impl SelectorVisitor for SiblingCombinatorAfterScopeSelectorVisitor { type Impl = SelectorImpl; fn visit_simple_selector(&mut self, c: &Component) -> bool { if !matches!(c, Component::Scope | Component::ImplicitScope) { return true; } // e.g. `:scope ~ .a` will never match. if self.right_combinator_is_sibling { self.found = true; } true } fn visit_selector_list( &mut self, _list_kind: SelectorListKind, list: &[Selector], ) -> bool { for s in list { let list_visitor = Self::new(self.right_combinator_is_sibling); self.found |= list_visitor.find_never_matching_scope_selector(s); } true } fn visit_complex_selector(&mut self, combinator_to_right: Option) -> bool { if let Some(c) = combinator_to_right { // Subject compounds' state is determined by the outer visitor. e.g: When there's `:is(.a .b) ~ .c`, // the inner visitor is assumed to be constructed with right_combinator_is_sibling == true. self.right_combinator_is_sibling = c.is_sibling(); } true } // It's harder to discern if use of :scope is invalid - at least for now, defer. } ================================================ FILE: style/font_face.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The [`@font-face`][ff] at-rule. //! //! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule use crate::derives::*; use crate::error_reporting::ContextualParseError; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; use crate::values::generics::font::FontStyle as GenericFontStyle; use crate::values::specified::{url::SpecifiedUrl, Angle}; use cssparser::{Parser, RuleBodyParser, SourceLocation}; use std::fmt::{self, Write}; use style_traits::{CssStringWriter, CssWriter, ParseError, StyleParseErrorKind, ToCss}; pub use crate::properties::font_face::{DescriptorId, DescriptorParser, Descriptors}; pub use crate::values::computed::font::{FamilyName, FontStretch}; pub use crate::values::specified::font::{ AbsoluteFontWeight, FontFeatureSettings, FontLanguageOverride, FontStretch as SpecifiedFontStretch, FontVariationSettings, MetricsOverride, SpecifiedFontStyle, }; /// A source for a font-face rule. #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] pub enum Source { /// A `url()` source. Url(UrlSource), /// A `local()` source. #[css(function)] Local(FamilyName), } /// A list of sources for the font-face src descriptor. #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] #[css(comma)] pub struct SourceList(#[css(iterable)] pub Vec); // We can't just use OneOrMoreSeparated to derive Parse for the Source list, // because we want to filter out components that parsed as None, then fail if no // valid components remain. So we provide our own implementation here. impl Parse for SourceList { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { // Parse the comma-separated list, then let filter_map discard any None items. let list = input .parse_comma_separated(|input| { let s = input.parse_entirely(|input| Source::parse(context, input)); while input.next().is_ok() {} Ok(s.ok()) })? .into_iter() .filter_map(|s| s) .collect::>(); if list.is_empty() { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(SourceList(list)) } } } /// Keywords for the font-face src descriptor's format() function. /// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.) #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(u8)] #[allow(missing_docs)] pub enum FontFaceSourceFormatKeyword { #[css(skip)] None, Collection, EmbeddedOpentype, Opentype, Svg, Truetype, Woff, Woff2, #[css(skip)] Unknown, } /// Flags for the @font-face tech() function, indicating font technologies /// required by the resource. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(C)] pub struct FontFaceSourceTechFlags(u16); bitflags! { impl FontFaceSourceTechFlags: u16 { /// Font requires OpenType feature support. const FEATURES_OPENTYPE = 1 << 0; /// Font requires Apple Advanced Typography support. const FEATURES_AAT = 1 << 1; /// Font requires Graphite shaping support. const FEATURES_GRAPHITE = 1 << 2; /// Font requires COLRv0 rendering support (simple list of colored layers). const COLOR_COLRV0 = 1 << 3; /// Font requires COLRv1 rendering support (graph of paint operations). const COLOR_COLRV1 = 1 << 4; /// Font requires SVG glyph rendering support. const COLOR_SVG = 1 << 5; /// Font has bitmap glyphs in 'sbix' format. const COLOR_SBIX = 1 << 6; /// Font has bitmap glyphs in 'CBDT' format. const COLOR_CBDT = 1 << 7; /// Font requires OpenType Variations support. const VARIATIONS = 1 << 8; /// Font requires CPAL palette selection support. const PALETTES = 1 << 9; /// Font requires support for incremental downloading. const INCREMENTAL = 1 << 10; } } impl FontFaceSourceTechFlags { /// Parse a single font-technology keyword and return its flag. pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { Ok(try_match_ident_ignore_ascii_case! { input, "features-opentype" => Self::FEATURES_OPENTYPE, "features-aat" => Self::FEATURES_AAT, "features-graphite" => Self::FEATURES_GRAPHITE, "color-colrv0" => Self::COLOR_COLRV0, "color-colrv1" => Self::COLOR_COLRV1, "color-svg" => Self::COLOR_SVG, "color-sbix" => Self::COLOR_SBIX, "color-cbdt" => Self::COLOR_CBDT, "variations" => Self::VARIATIONS, "palettes" => Self::PALETTES, "incremental" => Self::INCREMENTAL, }) } } impl Parse for FontFaceSourceTechFlags { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); // We don't actually care about the return value of parse_comma_separated, // because we insert the flags into result as we go. let mut result = Self::empty(); input.parse_comma_separated(|input| { let flag = Self::parse_one(input)?; result.insert(flag); Ok(()) })?; if !result.is_empty() { Ok(result) } else { Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } } } #[allow(unused_assignments)] impl ToCss for FontFaceSourceTechFlags { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { let mut first = true; macro_rules! write_if_flag { ($s:expr => $f:ident) => { if self.contains(Self::$f) { if first { first = false; } else { dest.write_str(", ")?; } dest.write_str($s)?; } }; } write_if_flag!("features-opentype" => FEATURES_OPENTYPE); write_if_flag!("features-aat" => FEATURES_AAT); write_if_flag!("features-graphite" => FEATURES_GRAPHITE); write_if_flag!("color-colrv0" => COLOR_COLRV0); write_if_flag!("color-colrv1" => COLOR_COLRV1); write_if_flag!("color-svg" => COLOR_SVG); write_if_flag!("color-sbix" => COLOR_SBIX); write_if_flag!("color-cbdt" => COLOR_CBDT); write_if_flag!("variations" => VARIATIONS); write_if_flag!("palettes" => PALETTES); write_if_flag!("incremental" => INCREMENTAL); Ok(()) } } /// #[derive(Clone, Debug, ToShmem, PartialEq)] pub struct FontFaceRule { /// The descriptors of the @font-face rule. pub descriptors: Descriptors, /// The parser location of the rule. pub source_location: SourceLocation, } impl FontFaceRule { /// Returns an empty rule. pub fn empty(source_location: SourceLocation) -> Self { Self { descriptors: Default::default(), source_location, } } } /// A POD representation for Gecko. All pointers here are non-owned and as such /// can't outlive the rule they came from, but we can't enforce that via C++. /// /// All the strings are of course utf8. #[cfg(feature = "gecko")] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] #[allow(missing_docs)] pub enum FontFaceSourceListComponent { Url(*const crate::gecko::url::CssUrl), Local(*mut crate::gecko_bindings::structs::nsAtom), FormatHintKeyword(FontFaceSourceFormatKeyword), FormatHintString { length: usize, utf8_bytes: *const u8, }, TechFlags(FontFaceSourceTechFlags), } #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[repr(u8)] #[allow(missing_docs)] pub enum FontFaceSourceFormat { Keyword(FontFaceSourceFormatKeyword), String(String), } /// A `UrlSource` represents a font-face source that has been specified with a /// `url()` function. /// /// #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] pub struct UrlSource { /// The specified url. pub url: SpecifiedUrl, /// The format hint specified with the `format()` function, if present. pub format_hint: Option, /// The font technology flags specified with the `tech()` function, if any. pub tech_flags: FontFaceSourceTechFlags, } impl ToCss for UrlSource { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { self.url.to_css(dest)?; if let Some(hint) = &self.format_hint { dest.write_str(" format(")?; hint.to_css(dest)?; dest.write_char(')')?; } if !self.tech_flags.is_empty() { dest.write_str(" tech(")?; self.tech_flags.to_css(dest)?; dest.write_char(')')?; } Ok(()) } } /// A font-display value for a @font-face rule. /// The font-display descriptor determines how a font face is displayed based /// on whether and when it is downloaded and ready to use. #[allow(missing_docs)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem, )] #[repr(u8)] pub enum FontDisplay { Auto, Block, Swap, Fallback, Optional, } macro_rules! impl_range { ($range:ident, $component:ident) => { impl Parse for $range { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let first = $component::parse(context, input)?; let second = input .try_parse(|input| $component::parse(context, input)) .unwrap_or_else(|_| first.clone()); Ok($range(first, second)) } } impl ToCss for $range { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { self.0.to_css(dest)?; if self.0 != self.1 { dest.write_char(' ')?; self.1.to_css(dest)?; } Ok(()) } } }; } /// The font-weight descriptor: /// /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight); impl_range!(FontWeightRange, AbsoluteFontWeight); /// The computed representation of the above so Gecko can read them easily. /// /// This one is needed because cbindgen doesn't know how to generate /// specified::Number. #[repr(C)] #[allow(missing_docs)] pub struct ComputedFontWeightRange(f32, f32); #[inline] fn sort_range(a: T, b: T) -> (T, T) { if a > b { (b, a) } else { (a, b) } } impl FontWeightRange { /// Returns a computed font-stretch range. pub fn compute(&self) -> ComputedFontWeightRange { let (min, max) = sort_range(self.0.compute().value(), self.1.compute().value()); ComputedFontWeightRange(min, max) } } /// The font-stretch descriptor: /// /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch); impl_range!(FontStretchRange, SpecifiedFontStretch); /// The computed representation of the above, so that Gecko can read them /// easily. #[repr(C)] #[allow(missing_docs)] pub struct ComputedFontStretchRange(FontStretch, FontStretch); impl FontStretchRange { /// Returns a computed font-stretch range. pub fn compute(&self) -> ComputedFontStretchRange { fn compute_stretch(s: &SpecifiedFontStretch) -> FontStretch { match *s { SpecifiedFontStretch::Keyword(ref kw) => kw.compute(), SpecifiedFontStretch::Stretch(ref p) => FontStretch::from_percentage(p.0.get()), SpecifiedFontStretch::System(..) => unreachable!(), } } let (min, max) = sort_range(compute_stretch(&self.0), compute_stretch(&self.1)); ComputedFontStretchRange(min, max) } } /// The font-style descriptor: /// /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[allow(missing_docs)] pub enum FontStyle { Italic, Oblique(Angle, Angle), } /// The computed representation of the above, with angles in degrees, so that /// Gecko can read them easily. #[repr(u8)] #[allow(missing_docs)] pub enum ComputedFontStyleDescriptor { Italic, Oblique(f32, f32), } impl Parse for FontStyle { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { // We parse 'normal' explicitly here to distinguish it from 'oblique 0deg', // because we must not accept a following angle. if input .try_parse(|i| i.expect_ident_matching("normal")) .is_ok() { return Ok(FontStyle::Oblique(Angle::zero(), Angle::zero())); } let style = SpecifiedFontStyle::parse(context, input)?; Ok(match style { GenericFontStyle::Italic => FontStyle::Italic, GenericFontStyle::Oblique(angle) => { let second_angle = input .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input)) .unwrap_or_else(|_| angle.clone()); FontStyle::Oblique(angle, second_angle) }, }) } } impl ToCss for FontStyle { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { match *self { FontStyle::Italic => dest.write_str("italic"), FontStyle::Oblique(ref first, ref second) => { // Not first.is_zero() because we don't want to serialize // `oblique calc(0deg)` as `normal`. if *first == Angle::zero() && first == second { return dest.write_str("normal"); } dest.write_str("oblique")?; if *first != SpecifiedFontStyle::default_angle() || first != second { dest.write_char(' ')?; first.to_css(dest)?; } if first != second { dest.write_char(' ')?; second.to_css(dest)?; } Ok(()) }, } } } impl FontStyle { /// Returns a computed font-style descriptor. pub fn compute(&self) -> ComputedFontStyleDescriptor { match *self { FontStyle::Italic => ComputedFontStyleDescriptor::Italic, FontStyle::Oblique(ref first, ref second) => { let (min, max) = sort_range( SpecifiedFontStyle::compute_angle_degrees(first), SpecifiedFontStyle::compute_angle_degrees(second), ); ComputedFontStyleDescriptor::Oblique(min, max) }, } } } /// Parse the block inside a `@font-face` rule. /// /// Note that the prelude parsing code lives in the `stylesheets` module. pub fn parse_font_face_block( context: &ParserContext, input: &mut Parser, source_location: SourceLocation, ) -> FontFaceRule { let mut rule = FontFaceRule::empty(source_location); { let mut parser = DescriptorParser { context, descriptors: &mut rule.descriptors, }; let mut iter = RuleBodyParser::new(input, &mut parser); while let Some(declaration) = iter.next() { if let Err((error, slice)) = declaration { let location = error.location; let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error); context.log_css_error(location, error) } } } rule } impl Parse for Source { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|input| input.expect_function_matching("local")) .is_ok() { return input .parse_nested_block(|input| FamilyName::parse(context, input)) .map(Source::Local); } let url = SpecifiedUrl::parse(context, input)?; // Parsing optional format() let format_hint = if input .try_parse(|input| input.expect_function_matching("format")) .is_ok() { input.parse_nested_block(|input| { if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) { Ok(Some(FontFaceSourceFormat::Keyword(kw))) } else { let s = input.expect_string()?.as_ref().to_owned(); Ok(Some(FontFaceSourceFormat::String(s))) } })? } else { None }; // Parse optional tech() let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled") && input .try_parse(|input| input.expect_function_matching("tech")) .is_ok() { input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))? } else { FontFaceSourceTechFlags::empty() }; Ok(Source::Url(UrlSource { url, format_hint, tech_flags, })) } } impl ToCssWithGuard for FontFaceRule { // Serialization of FontFaceRule is not specced. fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { dest.write_str("@font-face { ")?; self.descriptors.to_css(&mut CssWriter::new(dest))?; dest.write_char('}') } } ================================================ FILE: style/font_metrics.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Access to font metrics from the style system. #![deny(missing_docs)] use crate::values::computed::Length; /// Represents the font metrics that style needs from a font to compute the /// value of certain CSS units like `ex`. #[derive(Clone, Debug, PartialEq)] pub struct FontMetrics { /// The x-height of the font. pub x_height: Option, /// The zero advance. This is usually writing mode dependent pub zero_advance_measure: Option, /// The cap-height of the font. pub cap_height: Option, /// The ideographic-width of the font. pub ic_width: Option, /// The ascent of the font (a value is always available for this). pub ascent: Length, /// Script scale down factor for math-depth 1. /// https://w3c.github.io/mathml-core/#dfn-scriptpercentscaledown pub script_percent_scale_down: Option, /// Script scale down factor for math-depth 2. /// https://w3c.github.io/mathml-core/#dfn-scriptscriptpercentscaledown pub script_script_percent_scale_down: Option, } impl Default for FontMetrics { fn default() -> Self { FontMetrics { x_height: None, zero_advance_measure: None, cap_height: None, ic_width: None, ascent: Length::new(0.0), script_percent_scale_down: None, script_script_percent_scale_down: None, } } } impl FontMetrics { /// Returns the x-height, computing a fallback value if not present pub fn x_height_or_default(&self, reference_font_size: Length) -> Length { // https://drafts.csswg.org/css-values/#ex // // In the cases where it is impossible or impractical to // determine the x-height, a value of 0.5em must be // assumed. // // (But note we use 0.5em of the used, not computed // font-size) self.x_height.unwrap_or_else(|| reference_font_size * 0.5) } /// Returns the zero advance measure, computing a fallback value if not present pub fn zero_advance_measure_or_default( &self, reference_font_size: Length, upright: bool, ) -> Length { // https://drafts.csswg.org/css-values/#ch // // In the cases where it is impossible or impractical to // determine the measure of the “0” glyph, it must be // assumed to be 0.5em wide by 1em tall. Thus, the ch // unit falls back to 0.5em in the general case, and to // 1em when it would be typeset upright (i.e. // writing-mode is vertical-rl or vertical-lr and // text-orientation is upright). // // Same caveat about computed vs. used font-size applies // above. self.zero_advance_measure.unwrap_or_else(|| { if upright { reference_font_size } else { reference_font_size * 0.5 } }) } /// Returns the cap-height, computing a fallback value if not present pub fn cap_height_or_default(&self) -> Length { // https://drafts.csswg.org/css-values/#cap // // In the cases where it is impossible or impractical to // determine the cap-height, the font’s ascent must be // used. // self.cap_height.unwrap_or_else(|| self.ascent) } /// Returns the ideographic advance measure, computing a fallback value if not present pub fn ic_width_or_default(&self, reference_font_size: Length) -> Length { // https://drafts.csswg.org/css-values/#ic // // In the cases where it is impossible or impractical to // determine the ideographic advance measure, it must be // assumed to be 1em. // // Same caveat about computed vs. used as for other // metric-dependent units. self.ic_width.unwrap_or_else(|| reference_font_size) } } /// Type of font metrics to retrieve. #[derive(Clone, Debug, PartialEq)] pub enum FontMetricsOrientation { /// Get metrics for horizontal or vertical according to the Context's /// writing mode, using horizontal metrics for vertical/mixed MatchContextPreferHorizontal, /// Get metrics for horizontal or vertical according to the Context's /// writing mode, using vertical metrics for vertical/mixed MatchContextPreferVertical, /// Force getting horizontal metrics. Horizontal, } ================================================ FILE: style/gecko/anon_boxes.toml ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # This file contains the list of anonymous boxes and some flags, much like # longhands.toml and pseudo_elements.toml. # # Each anon box has its own section, with the following keys: # inherits - whether the anon box inherits from its parent or not. Defaults to true. # wrapper - whether the anon box is a wrapper around other frames during # frame tree fix-up (e.g. table anonymous boxes, ruby anonymous boxes, anonymous # flex item blocks, etc). Defaults to false. # # When adding an anon box, you also need to add the relevant atom to # StaticAtoms.py, with the camel-case name of the anon box, in the same order as # this file. [-moz-oof-placeholder] inherits = false # Framesets [-moz-hframeset-border] inherits = false [-moz-vframeset-border] inherits = false [-moz-frameset-blank] inherits = false # Table [-moz-table-column-group] inherits = false [-moz-table-column] inherits = false [-moz-page] inherits = false [-moz-page-break] inherits = false [-moz-page-content] inherits = false [-moz-printed-sheet] inherits = false # Applies to blocks that wrap contiguous runs of "column-span: all" elements in # multi-column subtrees, or the wrappers themselves, all the way up to the # column set wrappers. [-moz-column-span-wrapper] inherits = false # ::-moz-text, ::-moz-oof-placeholder, and ::-moz-first-letter-continuation are # non-elements which no rule will match. [-moz-text] # nsFirstLetterFrames for content outside the ::first-letter. [-moz-first-letter-continuation] [-moz-block-inside-inline-wrapper] [-moz-mathml-anonymous-block] wrapper = true [-moz-line-frame] [-moz-cell-content] [-moz-fieldset-content] [-moz-html-canvas-content] [-moz-inline-table] wrapper = true [-moz-table] wrapper = true [-moz-table-cell] wrapper = true [-moz-table-wrapper] [-moz-table-row-group] wrapper = true [-moz-table-row] wrapper = true [-moz-canvas] inherits = false [-moz-page-sequence] [-moz-scrolled-content] # A column set is a set of columns inside of ColumnSetWrapperFrame, which # applies to nsColumnSetFrame. It doesn't contain any column-span elements. [-moz-column-set] [-moz-column-content] # The root (viewport) frame [-moz-viewport] inherits = false # The root scrollframe. [-moz-viewport-scroll] inherits = false # Inside a flex/grid/-moz-box container, a contiguous run of text gets wrapped # in an anonymous block, which is then treated as a flex item. [-moz-anonymous-item] wrapper = true [-moz-block-ruby-content] [-moz-ruby] wrapper = true [-moz-ruby-base] wrapper = true [-moz-ruby-base-container] wrapper = true [-moz-ruby-text] wrapper = true [-moz-ruby-text-container] wrapper = true [-moz-svg-marker-anon-child] [-moz-svg-outer-svg-anon-child] [-moz-svg-foreign-content] [-moz-svg-text] ================================================ FILE: style/gecko/arc_types.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! This file lists all arc FFI types and defines corresponding addref and release functions. This //! list loosely corresponds to ServoLockedArcTypeList.inc file in Gecko. #![allow(non_snake_case, missing_docs)] use crate::gecko::url::CssUrlData; use crate::media_queries::MediaList; use crate::properties::animated_properties::AnimationValue; use crate::properties::{ComputedValues, PropertyDeclarationBlock}; use crate::shared_lock::Locked; use crate::stylesheets::keyframes_rule::Keyframe; use crate::stylesheets::{ AppearanceBaseRule, ContainerRule, CssRules, CustomMediaRule, DocumentRule, FontFeatureValuesRule, FontPaletteValuesRule, LayerBlockRule, LayerStatementRule, MarginRule, MediaRule, NamespaceRule, PropertyRule, ScopeRule, StartingStyleRule, StylesheetContents, SupportsRule, ViewTransitionRule, }; pub use crate::stylesheets::{ LockedCounterStyleRule, LockedFontFaceRule, LockedImportRule, LockedKeyframesRule, LockedNestedDeclarationsRule, LockedPageRule, LockedPositionTryRule, LockedStyleRule, }; use servo_arc::Arc; macro_rules! impl_simple_arc_ffi { ($ty:ty, $addref:ident, $release:ident) => { #[no_mangle] pub unsafe extern "C" fn $addref(obj: *const $ty) { std::mem::forget(Arc::from_raw_addrefed(obj)); } #[no_mangle] pub unsafe extern "C" fn $release(obj: *const $ty) { let _ = Arc::from_raw(obj); } }; } macro_rules! impl_locked_arc_ffi { ($servo_type:ty, $alias:ident, $addref:ident, $release:ident) => { /// A simple alias for a locked type. pub type $alias = Locked<$servo_type>; impl_simple_arc_ffi!($alias, $addref, $release); }; } impl_locked_arc_ffi!( CssRules, LockedCssRules, Servo_CssRules_AddRef, Servo_CssRules_Release ); impl_locked_arc_ffi!( PropertyDeclarationBlock, LockedDeclarationBlock, Servo_DeclarationBlock_AddRef, Servo_DeclarationBlock_Release ); impl_simple_arc_ffi!( LockedStyleRule, Servo_StyleRule_AddRef, Servo_StyleRule_Release ); impl_simple_arc_ffi!( LockedImportRule, Servo_ImportRule_AddRef, Servo_ImportRule_Release ); impl_locked_arc_ffi!( Keyframe, LockedKeyframe, Servo_Keyframe_AddRef, Servo_Keyframe_Release ); impl_simple_arc_ffi!( LockedKeyframesRule, Servo_KeyframesRule_AddRef, Servo_KeyframesRule_Release ); impl_simple_arc_ffi!( LayerBlockRule, Servo_LayerBlockRule_AddRef, Servo_LayerBlockRule_Release ); impl_simple_arc_ffi!( LayerStatementRule, Servo_LayerStatementRule_AddRef, Servo_LayerStatementRule_Release ); impl_locked_arc_ffi!( MediaList, LockedMediaList, Servo_MediaList_AddRef, Servo_MediaList_Release ); impl_simple_arc_ffi!(MediaRule, Servo_MediaRule_AddRef, Servo_MediaRule_Release); impl_simple_arc_ffi!( CustomMediaRule, Servo_CustomMediaRule_AddRef, Servo_CustomMediaRule_Release ); impl_simple_arc_ffi!( NamespaceRule, Servo_NamespaceRule_AddRef, Servo_NamespaceRule_Release ); impl_simple_arc_ffi!( MarginRule, Servo_MarginRule_AddRef, Servo_MarginRule_Release ); impl_simple_arc_ffi!( LockedPageRule, Servo_PageRule_AddRef, Servo_PageRule_Release ); impl_simple_arc_ffi!( PropertyRule, Servo_PropertyRule_AddRef, Servo_PropertyRule_Release ); impl_simple_arc_ffi!( SupportsRule, Servo_SupportsRule_AddRef, Servo_SupportsRule_Release ); impl_simple_arc_ffi!( ContainerRule, Servo_ContainerRule_AddRef, Servo_ContainerRule_Release ); impl_simple_arc_ffi!( DocumentRule, Servo_DocumentRule_AddRef, Servo_DocumentRule_Release ); impl_simple_arc_ffi!( FontFeatureValuesRule, Servo_FontFeatureValuesRule_AddRef, Servo_FontFeatureValuesRule_Release ); impl_simple_arc_ffi!( FontPaletteValuesRule, Servo_FontPaletteValuesRule_AddRef, Servo_FontPaletteValuesRule_Release ); impl_simple_arc_ffi!( LockedFontFaceRule, Servo_FontFaceRule_AddRef, Servo_FontFaceRule_Release ); impl_simple_arc_ffi!( LockedCounterStyleRule, Servo_CounterStyleRule_AddRef, Servo_CounterStyleRule_Release ); impl_simple_arc_ffi!( StylesheetContents, Servo_StyleSheetContents_AddRef, Servo_StyleSheetContents_Release ); impl_simple_arc_ffi!( CssUrlData, Servo_CssUrlData_AddRef, Servo_CssUrlData_Release ); impl_simple_arc_ffi!( ComputedValues, Servo_ComputedStyle_AddRef, Servo_ComputedStyle_Release ); impl_simple_arc_ffi!( AnimationValue, Servo_AnimationValue_AddRef, Servo_AnimationValue_Release ); impl_simple_arc_ffi!(ScopeRule, Servo_ScopeRule_AddRef, Servo_ScopeRule_Release); impl_simple_arc_ffi!( StartingStyleRule, Servo_StartingStyleRule_AddRef, Servo_StartingStyleRule_Release ); impl_simple_arc_ffi!( AppearanceBaseRule, Servo_AppearanceBaseRule_AddRef, Servo_AppearanceBaseRule_Release ); impl_simple_arc_ffi!( LockedPositionTryRule, Servo_PositionTryRule_AddRef, Servo_PositionTryRule_Release ); impl_simple_arc_ffi!( LockedNestedDeclarationsRule, Servo_NestedDeclarationsRule_AddRef, Servo_NestedDeclarationsRule_Release ); impl_simple_arc_ffi!( ViewTransitionRule, Servo_ViewTransitionRule_AddRef, Servo_ViewTransitionRule_Release ); ================================================ FILE: style/gecko/conversions.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! This module contains conversion helpers between Servo and Gecko types //! Ideally, it would be in geckolib itself, but coherence //! forces us to keep the traits and implementations here //! //! FIXME(emilio): This file should generally just die. #![allow(unsafe_code)] use crate::gecko_bindings::structs::{nsresult, Matrix4x4Components}; use crate::stylesheets::RulesMutateError; use crate::values::computed::transform::Matrix3D; impl From for nsresult { fn from(other: RulesMutateError) -> Self { match other { RulesMutateError::Syntax => nsresult::NS_ERROR_DOM_SYNTAX_ERR, RulesMutateError::IndexSize => nsresult::NS_ERROR_DOM_INDEX_SIZE_ERR, RulesMutateError::HierarchyRequest => nsresult::NS_ERROR_DOM_HIERARCHY_REQUEST_ERR, RulesMutateError::InvalidState => nsresult::NS_ERROR_DOM_INVALID_STATE_ERR, } } } impl<'a> From<&'a Matrix4x4Components> for Matrix3D { fn from(m: &'a Matrix4x4Components) -> Matrix3D { Matrix3D { m11: m[0], m12: m[1], m13: m[2], m14: m[3], m21: m[4], m22: m[5], m23: m[6], m24: m[7], m31: m[8], m32: m[9], m33: m[10], m34: m[11], m41: m[12], m42: m[13], m43: m[14], m44: m[15], } } } impl From for Matrix4x4Components { fn from(matrix: Matrix3D) -> Self { [ matrix.m11, matrix.m12, matrix.m13, matrix.m14, matrix.m21, matrix.m22, matrix.m23, matrix.m24, matrix.m31, matrix.m32, matrix.m33, matrix.m34, matrix.m41, matrix.m42, matrix.m43, matrix.m44, ] } } ================================================ FILE: style/gecko/data.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Data needed to style a Gecko document. use crate::derives::*; use crate::device::Device; use crate::gecko_bindings::bindings; use crate::gecko_bindings::structs::{ self, ServoStyleSetSizes, StyleSheet as DomStyleSheet, StyleSheetInfo, }; use crate::invalidation::stylesheets::StylesheetInvalidationSet; use crate::media_queries::MediaList; use crate::properties::ComputedValues; use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards}; use crate::stylesheets::scope_rule::ImplicitScopeRoot; use crate::stylesheets::{StylesheetContents, StylesheetInDocument}; use crate::stylist::Stylist; use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; use malloc_size_of::MallocSizeOfOps; use selectors::Element; use servo_arc::Arc; use std::fmt; use super::wrapper::GeckoElement; /// Little wrapper to a Gecko style sheet. #[derive(Eq, PartialEq)] pub struct GeckoStyleSheet(*const DomStyleSheet); // NOTE(emilio): These are kind of a lie. We allow to make these Send + Sync so that other data // structures can also be Send and Sync, but Gecko's stylesheets are main-thread-reference-counted. // // We assert that we reference-count in the right thread (in the Addref/Release implementations). // Sending these to a different thread can't really happen (it could theoretically really happen if // we allowed @import rules inside a nested style rule, but that can't happen per spec and would be // a parser bug, caught by the asserts). unsafe impl Send for GeckoStyleSheet {} unsafe impl Sync for GeckoStyleSheet {} impl fmt::Debug for GeckoStyleSheet { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { let contents = self.raw_contents(); formatter .debug_struct("GeckoStyleSheet") .field("origin", &contents.origin) .field("url_data", &contents.url_data) .finish() } } impl GeckoStyleSheet { /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer. #[inline] pub unsafe fn new(s: *const DomStyleSheet) -> Self { debug_assert!(!s.is_null()); bindings::Gecko_StyleSheet_AddRef(s); Self::from_addrefed(s) } /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer that /// already holds a strong reference. #[inline] pub unsafe fn from_addrefed(s: *const DomStyleSheet) -> Self { assert!(!s.is_null()); GeckoStyleSheet(s) } /// HACK(emilio): This is so that we can avoid crashing release due to /// bug 1719963 and can hopefully get a useful report from fuzzers. #[inline] pub fn hack_is_null(&self) -> bool { self.0.is_null() } /// Get the raw `StyleSheet` that we're wrapping. pub fn raw(&self) -> &DomStyleSheet { unsafe { &*self.0 } } fn inner(&self) -> &StyleSheetInfo { unsafe { &*(self.raw().mInner as *const StyleSheetInfo) } } fn raw_contents(&self) -> &StylesheetContents { debug_assert!(!self.inner().mContents.mRawPtr.is_null()); unsafe { &*self.inner().mContents.mRawPtr } } } impl Drop for GeckoStyleSheet { fn drop(&mut self) { unsafe { bindings::Gecko_StyleSheet_Release(self.0) }; } } impl Clone for GeckoStyleSheet { fn clone(&self) -> Self { unsafe { bindings::Gecko_StyleSheet_AddRef(self.0) }; GeckoStyleSheet(self.0) } } impl StylesheetInDocument for GeckoStyleSheet { fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList; unsafe { let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList; if dom_media_list.is_null() { return None; } let list = &*(*dom_media_list).mRawList.mRawPtr; Some(list.read_with(guard)) } } // All the stylesheets Servo knows about are enabled, because that state is // handled externally by Gecko. #[inline] fn enabled(&self) -> bool { true } #[inline] fn contents<'a>(&'a self, _: &'a SharedRwLockReadGuard) -> &'a StylesheetContents { self.raw_contents() } fn implicit_scope_root(&self) -> Option { unsafe { let result = bindings::Gecko_StyleSheet_ImplicitScopeRoot(self.0); if result.mRoot.is_null() { return if result.mConstructed { Some(ImplicitScopeRoot::Constructed) } else { // Could be genuinely not attached, like user stylesheet. None }; } let root = GeckoElement(result.mRoot.as_ref().unwrap()).opaque(); Some(if !result.mHost.is_null() { let host = GeckoElement(result.mHost.as_ref().unwrap()).opaque(); if host == root { ImplicitScopeRoot::ShadowHost(root) } else { ImplicitScopeRoot::InShadowTree(root) } } else { ImplicitScopeRoot::InLightTree(root) }) } } } /// The container for data that a Servo-backed Gecko document needs to style /// itself. pub struct PerDocumentStyleDataImpl { /// Rule processor. pub stylist: Stylist, /// A cache from element to resolved style. pub undisplayed_style_cache: crate::traversal::UndisplayedStyleCache, /// The generation for which our cache is valid. pub undisplayed_style_cache_generation: u64, } /// The data itself is an `AtomicRefCell`, which guarantees the proper semantics /// and unexpected races while trying to mutate it. #[derive(Deref)] pub struct PerDocumentStyleData(AtomicRefCell); impl PerDocumentStyleData { /// Create a `PerDocumentStyleData`. pub fn new(document: *const structs::Document) -> Self { let device = Device::new(document); let quirks_mode = device.document().mCompatMode; PerDocumentStyleData(AtomicRefCell::new(PerDocumentStyleDataImpl { stylist: Stylist::new(device, quirks_mode.into()), undisplayed_style_cache: Default::default(), undisplayed_style_cache_generation: 0, })) } /// Get an immutable reference to this style data. pub fn borrow(&self) -> AtomicRef<'_, PerDocumentStyleDataImpl> { self.0.borrow() } /// Get an mutable reference to this style data. pub fn borrow_mut(&self) -> AtomicRefMut<'_, PerDocumentStyleDataImpl> { self.0.borrow_mut() } } impl PerDocumentStyleDataImpl { /// Recreate the style data if the stylesheets have changed. pub fn flush_stylesheets( &mut self, guard: &SharedRwLockReadGuard, ) -> StylesheetInvalidationSet { self.stylist.flush(&StylesheetGuards::same(guard)) } /// Get the default computed values for this document. pub fn default_computed_values(&self) -> &Arc { self.stylist.device().default_computed_values_arc() } /// Measure heap usage. pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { self.stylist.add_size_of(ops, sizes); } } /// The gecko-specific AuthorStyles instantiation. pub type AuthorStyles = crate::author_styles::AuthorStyles; ================================================ FILE: style/gecko/media_features.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Gecko's media feature list and evaluator. use crate::derives::*; use crate::device::Device; use crate::gecko_bindings::bindings; use crate::gecko_bindings::structs; use crate::media_queries::MediaType; use crate::parser::ParserContext; use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; use crate::queries::values::{Orientation, PrefersColorScheme}; use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; use crate::values::specified::color::ForcedColors; use app_units::Au; use euclid::default::Size2D; fn device_size(device: &Device) -> Size2D { let mut width = 0; let mut height = 0; unsafe { bindings::Gecko_MediaFeatures_GetDeviceSize(device.document(), &mut width, &mut height); } Size2D::new(Au(width), Au(height)) } /// https://drafts.csswg.org/mediaqueries-4/#width fn eval_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) } /// https://drafts.csswg.org/mediaqueries-4/#device-width fn eval_device_width(context: &Context) -> CSSPixelLength { CSSPixelLength::new(device_size(context.device()).width.to_f32_px()) } /// https://drafts.csswg.org/mediaqueries-4/#height fn eval_height(context: &Context) -> CSSPixelLength { CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px()) } /// https://drafts.csswg.org/mediaqueries-4/#device-height fn eval_device_height(context: &Context) -> CSSPixelLength { CSSPixelLength::new(device_size(context.device()).height.to_f32_px()) } fn eval_aspect_ratio_for(context: &Context, get_size: F) -> Ratio where F: FnOnce(&Device) -> Size2D, { let size = get_size(context.device()); Ratio::new(size.width.0 as f32, size.height.0 as f32) } /// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio fn eval_aspect_ratio(context: &Context) -> Ratio { eval_aspect_ratio_for(context, Device::au_viewport_size) } /// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio fn eval_device_aspect_ratio(context: &Context) -> Ratio { eval_aspect_ratio_for(context, device_size) } /// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio fn eval_device_pixel_ratio(context: &Context) -> f32 { eval_resolution(context).dppx() } /// https://drafts.csswg.org/mediaqueries-4/#orientation fn eval_orientation(context: &Context, value: Option) -> bool { Orientation::eval(context.device().au_viewport_size(), value) } /// FIXME: There's no spec for `-moz-device-orientation`. fn eval_device_orientation(context: &Context, value: Option) -> bool { Orientation::eval(device_size(context.device()), value) } fn document_picture_in_picture_enabled(context: &ParserContext) -> bool { static_prefs::pref!("dom.documentpip.enabled") || context.chrome_rules_enabled() } /// Values for the display-mode media feature. #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] #[repr(u8)] #[allow(missing_docs)] pub enum DisplayMode { Browser = 0, MinimalUi, Standalone, Fullscreen, #[parse(condition = "document_picture_in_picture_enabled")] PictureInPicture, } /// https://w3c.github.io/manifest/#the-display-mode-media-feature fn eval_display_mode(context: &Context, query_value: Option) -> bool { match query_value { Some(v) => { v == unsafe { bindings::Gecko_MediaFeatures_GetDisplayMode(context.device().document()) } }, None => true, } } /// https://drafts.csswg.org/mediaqueries-4/#grid fn eval_grid(_: &Context) -> bool { // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature // is always 0. false } /// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d fn eval_transform_3d(_: &Context) -> bool { true } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Scan { Progressive, Interlace, } /// https://drafts.csswg.org/mediaqueries-4/#scan fn eval_scan(_: &Context, _: Option) -> bool { // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never // matches. false } /// https://drafts.csswg.org/mediaqueries-4/#color fn eval_color(context: &Context) -> i32 { unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(context.device().document()) } } /// https://drafts.csswg.org/mediaqueries-4/#color-index fn eval_color_index(_: &Context) -> i32 { // We should return zero if the device does not use a color lookup table. 0 } /// https://drafts.csswg.org/mediaqueries-4/#monochrome fn eval_monochrome(context: &Context) -> i32 { // For color devices we should return 0. unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(context.device().document()) } } /// Values for the color-gamut media feature. /// This implements PartialOrd so that lower values will correctly match /// higher capabilities. #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] #[repr(u8)] pub enum ColorGamut { /// The sRGB gamut. Srgb, /// The gamut specified by the Display P3 Color Space. P3, /// The gamut specified by the ITU-R Recommendation BT.2020 Color Space. Rec2020, } /// https://drafts.csswg.org/mediaqueries-4/#color-gamut fn eval_color_gamut(context: &Context, query_value: Option) -> bool { let query_value = match query_value { Some(v) => v, None => return false, }; let color_gamut = unsafe { bindings::Gecko_MediaFeatures_ColorGamut(context.device().document()) }; // Match if our color gamut is at least as wide as the query value query_value <= color_gamut } /// https://drafts.csswg.org/mediaqueries-4/#resolution fn eval_resolution(context: &Context) -> Resolution { let resolution_dppx = unsafe { bindings::Gecko_MediaFeatures_GetResolution(context.device().document()) }; Resolution::from_dppx(resolution_dppx) } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum PrefersReducedMotion { NoPreference, Reduce, } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum PrefersReducedTransparency { NoPreference, Reduce, } /// Values for the dynamic-range and video-dynamic-range media features. /// https://drafts.csswg.org/mediaqueries-5/#dynamic-range /// This implements PartialOrd so that lower values will correctly match /// higher capabilities. #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] #[repr(u8)] #[allow(missing_docs)] pub enum DynamicRange { Standard, High, } /// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion fn eval_prefers_reduced_motion( context: &Context, query_value: Option, ) -> bool { let prefers_reduced = unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(context.device().document()) }; let query_value = match query_value { Some(v) => v, None => return prefers_reduced, }; match query_value { PrefersReducedMotion::NoPreference => !prefers_reduced, PrefersReducedMotion::Reduce => prefers_reduced, } } /// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency fn eval_prefers_reduced_transparency( context: &Context, query_value: Option, ) -> bool { let prefers_reduced = unsafe { bindings::Gecko_MediaFeatures_PrefersReducedTransparency(context.device().document()) }; let query_value = match query_value { Some(v) => v, None => return prefers_reduced, }; match query_value { PrefersReducedTransparency::NoPreference => !prefers_reduced, PrefersReducedTransparency::Reduce => prefers_reduced, } } /// Possible values for prefers-contrast media query. /// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] #[repr(u8)] pub enum PrefersContrast { /// More contrast is preferred. More, /// Low contrast is preferred. Less, /// Custom (not more, not less). Custom, /// The default value if neither high or low contrast is enabled. NoPreference, } /// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast fn eval_prefers_contrast(context: &Context, query_value: Option) -> bool { let prefers_contrast = unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(context.device().document()) }; match query_value { Some(v) => v == prefers_contrast, None => prefers_contrast != PrefersContrast::NoPreference, } } /// https://drafts.csswg.org/mediaqueries-5/#forced-colors fn eval_forced_colors(context: &Context, query_value: Option) -> bool { let forced = context.device().forced_colors(); match query_value { Some(query_value) => query_value == forced, None => forced != ForcedColors::None, } } /// Possible values for the inverted-colors media query. /// https://drafts.csswg.org/mediaqueries-5/#inverted #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum InvertedColors { /// Colors are displayed normally. None, /// All pixels within the displayed area have been inverted. Inverted, } /// https://drafts.csswg.org/mediaqueries-5/#inverted fn eval_inverted_colors(context: &Context, query_value: Option) -> bool { let inverted_colors = unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) }; let query_value = match query_value { Some(v) => v, None => return inverted_colors, }; match query_value { InvertedColors::None => !inverted_colors, InvertedColors::Inverted => inverted_colors, } } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum OverflowBlock { None, Scroll, Paged, } /// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block fn eval_overflow_block(context: &Context, query_value: Option) -> bool { // For the time being, assume that printing (including previews) // is the only time when we paginate, and we are otherwise always // scrolling. This is true at the moment in Firefox, but may need // updating in the future (e.g., ebook readers built with Stylo, a // billboard mode that doesn't support overflow at all). // // If this ever changes, don't forget to change eval_overflow_inline too. let scrolling = context.device().media_type() != MediaType::print(); let query_value = match query_value { Some(v) => v, None => return true, }; match query_value { OverflowBlock::None => false, OverflowBlock::Scroll => scrolling, OverflowBlock::Paged => !scrolling, } } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum OverflowInline { None, Scroll, } /// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-inline fn eval_overflow_inline(context: &Context, query_value: Option) -> bool { // See the note in eval_overflow_block. let scrolling = context.device().media_type() != MediaType::print(); let query_value = match query_value { Some(v) => v, None => return scrolling, }; match query_value { OverflowInline::None => !scrolling, OverflowInline::Scroll => scrolling, } } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Update { None, Slow, Fast, } /// https://drafts.csswg.org/mediaqueries-4/#update fn eval_update(context: &Context, query_value: Option) -> bool { // This has similar caveats to those described in eval_overflow_block. // For now, we report that print (incl. print media simulation, // which can in fact update but is limited to the developer tools) // is `update: none` and that all other contexts are `update: fast`, // which may not be true for future platforms, like e-ink devices. let can_update = context.device().media_type() != MediaType::print(); let query_value = match query_value { Some(v) => v, None => return can_update, }; match query_value { Update::None => !can_update, Update::Slow => false, Update::Fast => can_update, } } fn do_eval_prefers_color_scheme( context: &Context, use_content: bool, query_value: Option, ) -> bool { let prefers_color_scheme = unsafe { bindings::Gecko_MediaFeatures_PrefersColorScheme(context.device().document(), use_content) }; match query_value { Some(v) => prefers_color_scheme == v, None => true, } } /// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme fn eval_prefers_color_scheme(context: &Context, query_value: Option) -> bool { do_eval_prefers_color_scheme(context, /* use_content = */ false, query_value) } fn eval_content_prefers_color_scheme( context: &Context, query_value: Option, ) -> bool { do_eval_prefers_color_scheme(context, /* use_content = */ true, query_value) } /// https://drafts.csswg.org/mediaqueries-5/#dynamic-range fn eval_dynamic_range(context: &Context, query_value: Option) -> bool { let dynamic_range = unsafe { bindings::Gecko_MediaFeatures_DynamicRange(context.device().document()) }; match query_value { Some(v) => dynamic_range >= v, None => false, } } /// https://drafts.csswg.org/mediaqueries-5/#video-dynamic-range fn eval_video_dynamic_range(context: &Context, query_value: Option) -> bool { let dynamic_range = unsafe { bindings::Gecko_MediaFeatures_VideoDynamicRange(context.device().document()) }; match query_value { Some(v) => dynamic_range >= v, None => false, } } bitflags! { /// https://drafts.csswg.org/mediaqueries-4/#mf-interaction struct PointerCapabilities: u8 { const COARSE = structs::PointerCapabilities_Coarse; const FINE = structs::PointerCapabilities_Fine; const HOVER = structs::PointerCapabilities_Hover; } } fn primary_pointer_capabilities(context: &Context) -> PointerCapabilities { PointerCapabilities::from_bits_truncate(unsafe { bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(context.device().document()) }) } fn all_pointer_capabilities(context: &Context) -> PointerCapabilities { PointerCapabilities::from_bits_truncate(unsafe { bindings::Gecko_MediaFeatures_AllPointerCapabilities(context.device().document()) }) } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Pointer { None, Coarse, Fine, } fn eval_pointer_capabilities( query_value: Option, pointer_capabilities: PointerCapabilities, ) -> bool { let query_value = match query_value { Some(v) => v, None => return !pointer_capabilities.is_empty(), }; match query_value { Pointer::None => pointer_capabilities.is_empty(), Pointer::Coarse => pointer_capabilities.intersects(PointerCapabilities::COARSE), Pointer::Fine => pointer_capabilities.intersects(PointerCapabilities::FINE), } } /// https://drafts.csswg.org/mediaqueries-4/#pointer fn eval_pointer(context: &Context, query_value: Option) -> bool { eval_pointer_capabilities(query_value, primary_pointer_capabilities(context)) } /// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer fn eval_any_pointer(context: &Context, query_value: Option) -> bool { eval_pointer_capabilities(query_value, all_pointer_capabilities(context)) } #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] enum Hover { None, Hover, } fn eval_hover_capabilities( query_value: Option, pointer_capabilities: PointerCapabilities, ) -> bool { let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER); let query_value = match query_value { Some(v) => v, None => return can_hover, }; match query_value { Hover::None => !can_hover, Hover::Hover => can_hover, } } /// https://drafts.csswg.org/mediaqueries-4/#hover fn eval_hover(context: &Context, query_value: Option) -> bool { eval_hover_capabilities(query_value, primary_pointer_capabilities(context)) } /// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover fn eval_any_hover(context: &Context, query_value: Option) -> bool { eval_hover_capabilities(query_value, all_pointer_capabilities(context)) } fn eval_moz_is_glyph(context: &Context) -> bool { context.device().document().mIsSVGGlyphsDocument() } fn eval_moz_in_android_pip_mode(context: &Context) -> bool { unsafe { bindings::Gecko_MediaFeatures_InAndroidPipMode(context.device().document()) } } fn eval_moz_print_preview(context: &Context) -> bool { let is_print_preview = context.device().is_print_preview(); if is_print_preview { debug_assert_eq!(context.device().media_type(), MediaType::print()); } is_print_preview } fn eval_moz_is_resource_document(context: &Context) -> bool { unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(context.device().document()) } } /// Allows front-end CSS to discern platform via media queries. #[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] #[repr(u8)] pub enum Platform { /// Matches any Android version. Android, /// For our purposes here, "linux" is just "gtk" (so unix-but-not-mac). /// There's no need for our front-end code to differentiate between those /// platforms and they already use the "linux" string elsewhere (e.g., /// toolkit/themes/linux). Linux, /// Matches any iOS version. Ios, /// Matches any macOS version. Macos, /// Matches any Windows version. Windows, } fn eval_moz_platform(_: &Context, query_value: Option) -> bool { let query_value = match query_value { Some(v) => v, None => return false, }; unsafe { bindings::Gecko_MediaFeatures_MatchesPlatform(query_value) } } /// Allows front-end CSS to discern gtk theme via media queries. #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] #[repr(u8)] pub enum GtkThemeFamily { /// Unknown theme family. Unknown = 0, /// Adwaita, the default GTK theme. Adwaita, /// Breeze, the default KDE theme. Breeze, /// Yaru, the default Ubuntu theme. Yaru, } fn eval_gtk_theme_family(_: &Context, query_value: Option) -> bool { let family = unsafe { bindings::Gecko_MediaFeatures_GtkThemeFamily() }; match query_value { Some(v) => v == family, None => return family != GtkThemeFamily::Unknown, } } /// Values for the scripting media feature. /// https://drafts.csswg.org/mediaqueries-5/#scripting #[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] #[repr(u8)] pub enum Scripting { /// Scripting is not supported or not enabled None, /// Scripting is supported and enabled, but only for initial page load /// We will never match this value as it is intended for non-browser user agents, /// but it is part of the spec so we should still parse it. /// See: https://github.com/w3c/csswg-drafts/issues/8621 InitialOnly, /// Scripting is supported and enabled Enabled, } /// https://drafts.csswg.org/mediaqueries-5/#scripting fn eval_scripting(context: &Context, query_value: Option) -> bool { let scripting = unsafe { bindings::Gecko_MediaFeatures_Scripting(context.device().document()) }; match query_value { Some(v) => v == scripting, None => scripting != Scripting::None, } } fn eval_moz_overlay_scrollbars(context: &Context) -> bool { unsafe { bindings::Gecko_MediaFeatures_UseOverlayScrollbars(context.device().document()) } } fn eval_moz_mac_rtl(context: &Context) -> bool { unsafe { bindings::Gecko_MediaFeatures_MacRTL(context.device().document()) } } fn eval_moz_native_theme(context: &Context) -> bool { if context.device().document().mForceNonNativeTheme() { return false; } static_prefs::pref!("browser.theme.native-theme") } fn get_lnf_int(int_id: i32) -> i32 { unsafe { bindings::Gecko_GetLookAndFeelInt(int_id) } } fn get_lnf_int_as_bool(int_id: i32) -> bool { get_lnf_int(int_id) != 0 } macro_rules! lnf_int_feature { ($feature_name:expr, $int_id:ident, $get_value:ident) => {{ fn __eval(_: &Context) -> bool { $get_value(bindings::LookAndFeel_IntID::$int_id as i32) } feature!( $feature_name, AllowsRanges::No, Evaluator::BoolInteger(__eval), FeatureFlags::CHROME_AND_UA_ONLY, ) }}; ($feature_name:expr, $int_id:ident) => {{ lnf_int_feature!($feature_name, $int_id, get_lnf_int_as_bool) }}; } /// Adding new media features requires (1) adding the new feature to this /// array, with appropriate entries (and potentially any new code needed /// to support new types in these entries and (2) ensuring that either /// nsPresContext::MediaFeatureValuesChanged is called when the value that /// would be returned by the evaluator function could change. pub static MEDIA_FEATURES: [QueryFeatureDescription; 60] = [ feature!( atom!("width"), AllowsRanges::Yes, Evaluator::Length(eval_width), FeatureFlags::VIEWPORT_DEPENDENT, ), feature!( atom!("height"), AllowsRanges::Yes, Evaluator::Length(eval_height), FeatureFlags::VIEWPORT_DEPENDENT, ), feature!( atom!("aspect-ratio"), AllowsRanges::Yes, Evaluator::NumberRatio(eval_aspect_ratio), FeatureFlags::VIEWPORT_DEPENDENT, ), feature!( atom!("orientation"), AllowsRanges::No, keyword_evaluator!(eval_orientation, Orientation), FeatureFlags::VIEWPORT_DEPENDENT, ), feature!( atom!("device-width"), AllowsRanges::Yes, Evaluator::Length(eval_device_width), FeatureFlags::empty(), ), feature!( atom!("device-height"), AllowsRanges::Yes, Evaluator::Length(eval_device_height), FeatureFlags::empty(), ), feature!( atom!("device-aspect-ratio"), AllowsRanges::Yes, Evaluator::NumberRatio(eval_device_aspect_ratio), FeatureFlags::empty(), ), feature!( atom!("-moz-device-orientation"), AllowsRanges::No, keyword_evaluator!(eval_device_orientation, Orientation), FeatureFlags::empty(), ), // Webkit extensions that we support for de-facto web compatibility. // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref): feature!( atom!("device-pixel-ratio"), AllowsRanges::Yes, Evaluator::Float(eval_device_pixel_ratio), FeatureFlags::WEBKIT_PREFIX, ), // -webkit-transform-3d. feature!( atom!("transform-3d"), AllowsRanges::No, Evaluator::BoolInteger(eval_transform_3d), FeatureFlags::WEBKIT_PREFIX, ), feature!( atom!("-moz-device-pixel-ratio"), AllowsRanges::Yes, Evaluator::Float(eval_device_pixel_ratio), FeatureFlags::empty(), ), feature!( atom!("resolution"), AllowsRanges::Yes, Evaluator::Resolution(eval_resolution), FeatureFlags::empty(), ), feature!( atom!("display-mode"), AllowsRanges::No, keyword_evaluator!(eval_display_mode, DisplayMode), FeatureFlags::empty(), ), feature!( atom!("grid"), AllowsRanges::No, Evaluator::BoolInteger(eval_grid), FeatureFlags::empty(), ), feature!( atom!("scan"), AllowsRanges::No, keyword_evaluator!(eval_scan, Scan), FeatureFlags::empty(), ), feature!( atom!("color"), AllowsRanges::Yes, Evaluator::Integer(eval_color), FeatureFlags::empty(), ), feature!( atom!("color-index"), AllowsRanges::Yes, Evaluator::Integer(eval_color_index), FeatureFlags::empty(), ), feature!( atom!("monochrome"), AllowsRanges::Yes, Evaluator::Integer(eval_monochrome), FeatureFlags::empty(), ), feature!( atom!("color-gamut"), AllowsRanges::No, keyword_evaluator!(eval_color_gamut, ColorGamut), FeatureFlags::empty(), ), feature!( atom!("prefers-reduced-motion"), AllowsRanges::No, keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), FeatureFlags::empty(), ), feature!( atom!("prefers-reduced-transparency"), AllowsRanges::No, keyword_evaluator!( eval_prefers_reduced_transparency, PrefersReducedTransparency ), FeatureFlags::empty(), ), feature!( atom!("prefers-contrast"), AllowsRanges::No, keyword_evaluator!(eval_prefers_contrast, PrefersContrast), FeatureFlags::empty(), ), feature!( atom!("forced-colors"), AllowsRanges::No, keyword_evaluator!(eval_forced_colors, ForcedColors), FeatureFlags::empty(), ), feature!( atom!("inverted-colors"), AllowsRanges::No, keyword_evaluator!(eval_inverted_colors, InvertedColors), FeatureFlags::empty(), ), feature!( atom!("overflow-block"), AllowsRanges::No, keyword_evaluator!(eval_overflow_block, OverflowBlock), FeatureFlags::empty(), ), feature!( atom!("overflow-inline"), AllowsRanges::No, keyword_evaluator!(eval_overflow_inline, OverflowInline), FeatureFlags::empty(), ), feature!( atom!("update"), AllowsRanges::No, keyword_evaluator!(eval_update, Update), FeatureFlags::empty(), ), feature!( atom!("prefers-color-scheme"), AllowsRanges::No, keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), FeatureFlags::empty(), ), feature!( atom!("dynamic-range"), AllowsRanges::No, keyword_evaluator!(eval_dynamic_range, DynamicRange), FeatureFlags::empty(), ), feature!( atom!("video-dynamic-range"), AllowsRanges::No, keyword_evaluator!(eval_video_dynamic_range, DynamicRange), FeatureFlags::empty(), ), feature!( atom!("scripting"), AllowsRanges::No, keyword_evaluator!(eval_scripting, Scripting), FeatureFlags::empty(), ), // Evaluates to the preferred color scheme for content. Only useful in // chrome context, where the chrome color-scheme and the content // color-scheme might differ. feature!( atom!("-moz-content-prefers-color-scheme"), AllowsRanges::No, keyword_evaluator!(eval_content_prefers_color_scheme, PrefersColorScheme), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("pointer"), AllowsRanges::No, keyword_evaluator!(eval_pointer, Pointer), FeatureFlags::empty(), ), feature!( atom!("any-pointer"), AllowsRanges::No, keyword_evaluator!(eval_any_pointer, Pointer), FeatureFlags::empty(), ), feature!( atom!("hover"), AllowsRanges::No, keyword_evaluator!(eval_hover, Hover), FeatureFlags::empty(), ), feature!( atom!("any-hover"), AllowsRanges::No, keyword_evaluator!(eval_any_hover, Hover), FeatureFlags::empty(), ), // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. // Internal because it is really only useful in the user agent anyway // and therefore not worth standardizing. feature!( atom!("-moz-is-glyph"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_is_glyph), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-in-android-pip-mode"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_in_android_pip_mode), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-is-resource-document"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_is_resource_document), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-platform"), AllowsRanges::No, keyword_evaluator!(eval_moz_platform, Platform), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-gtk-theme-family"), AllowsRanges::No, keyword_evaluator!(eval_gtk_theme_family, GtkThemeFamily), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-print-preview"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_print_preview), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-overlay-scrollbars"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_overlay_scrollbars), FeatureFlags::CHROME_AND_UA_ONLY, ), lnf_int_feature!(atom!("-moz-menubar-drag"), MenuBarDrag), lnf_int_feature!(atom!("-moz-mac-big-sur-theme"), MacBigSurTheme), lnf_int_feature!(atom!("-moz-mac-tahoe-theme"), MacTahoeTheme), feature!( atom!("-moz-mac-rtl"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_mac_rtl), FeatureFlags::CHROME_AND_UA_ONLY, ), feature!( atom!("-moz-native-theme"), AllowsRanges::No, Evaluator::BoolInteger(eval_moz_native_theme), FeatureFlags::CHROME_AND_UA_ONLY, ), lnf_int_feature!( atom!("-moz-windows-accent-color-in-titlebar"), WindowsAccentColorInTitlebar ), lnf_int_feature!(atom!("-moz-windows-mica"), WindowsMica), lnf_int_feature!(atom!("-moz-windows-mica-popups"), WindowsMicaPopups), lnf_int_feature!(atom!("-moz-swipe-animation-enabled"), SwipeAnimationEnabled), lnf_int_feature!(atom!("-moz-gtk-csd-available"), GTKCSDAvailable), lnf_int_feature!( atom!("-moz-gtk-csd-transparency-available"), GTKCSDTransparencyAvailable ), lnf_int_feature!(atom!("-moz-gtk-csd-minimize-button"), GTKCSDMinimizeButton), lnf_int_feature!(atom!("-moz-gtk-csd-maximize-button"), GTKCSDMaximizeButton), lnf_int_feature!(atom!("-moz-gtk-csd-close-button"), GTKCSDCloseButton), lnf_int_feature!( atom!("-moz-gtk-csd-reversed-placement"), GTKCSDReversedPlacement ), lnf_int_feature!(atom!("-moz-system-dark-theme"), SystemUsesDarkTheme), lnf_int_feature!(atom!("-moz-panel-animations"), PanelAnimations), ]; ================================================ FILE: style/gecko/mod.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Gecko-specific style-system bits. #[macro_use] mod non_ts_pseudo_class_list; pub mod arc_types; pub mod conversions; pub mod data; pub mod media_features; pub mod pseudo_element; pub mod restyle_damage; pub mod selector_parser; pub mod snapshot; pub mod snapshot_helpers; pub mod traversal; pub mod url; pub mod wrapper; ================================================ FILE: style/gecko/non_ts_pseudo_class_list.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /* * This file contains a helper macro includes all supported non-tree-structural * pseudo-classes. * * FIXME: Find a way to autogenerate this file. * * Expected usage is as follows: * ``` * macro_rules! pseudo_class_macro{ * ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { * // do stuff * } * } * apply_non_ts_list!(pseudo_class_macro) * ``` * * $state can be either "_" or an expression of type ElementState. If present, * the semantics are that the pseudo-class matches if any of the bits in * $state are set on the element. * $flags can be either "_" or an expression of type NonTSPseudoClassFlag, * see selector_parser.rs for more details. */ macro_rules! apply_non_ts_list { ($apply_macro:ident) => { $apply_macro! { [ ("-moz-table-border-nonzero", MozTableBorderNonzero, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-select-list-box", MozSelectListBox, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), ("link", Link, UNVISITED, _), ("any-link", AnyLink, VISITED_OR_UNVISITED, _), ("visited", Visited, VISITED, _), ("active", Active, ACTIVE, _), ("autofill", Autofill, AUTOFILL, _), ("checked", Checked, CHECKED, _), ("defined", Defined, DEFINED, _), ("disabled", Disabled, DISABLED, _), ("enabled", Enabled, ENABLED, _), ("focus", Focus, FOCUS, _), ("focus-within", FocusWithin, FOCUS_WITHIN, _), ("focus-visible", FocusVisible, FOCUSRING, _), ("has-slotted", HasSlotted, HAS_SLOTTED, _), ("hover", Hover, HOVER, _), ("active-view-transition", ActiveViewTransition, ACTIVE_VIEW_TRANSITION, _), ("-moz-drag-over", MozDragOver, DRAGOVER, _), ("target", Target, URLTARGET, _), ("indeterminate", Indeterminate, INDETERMINATE, _), ("-moz-inert", MozInert, INERT, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-devtools-highlighted", MozDevtoolsHighlighted, DEVTOOLS_HIGHLIGHTED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, STYLEEDITOR_TRANSITIONING, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("fullscreen", Fullscreen, FULLSCREEN, _), ("modal", Modal, MODAL, _), ("open", Open, OPEN, _), ("-moz-topmost-modal", MozTopmostModal, TOPMOST_MODAL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-broken", MozBroken, BROKEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), ("-moz-has-dir-attr", MozHasDirAttr, HAS_DIR_ATTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-dir-attr-ltr", MozDirAttrLTR, HAS_DIR_ATTR_LTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-dir-attr-rtl", MozDirAttrRTL, HAS_DIR_ATTR_RTL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, HAS_DIR_ATTR_LIKE_AUTO, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-autofill-preview", MozAutofillPreview, AUTOFILL_PREVIEW, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), ("-moz-value-empty", MozValueEmpty, VALUE_EMPTY, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-revealed", MozRevealed, REVEALED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-suppress-for-print-selection", MozSuppressForPrintSelection, SUPPRESS_FOR_PRINT_SELECTION, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, INCREMENT_SCRIPT_LEVEL, _), ("required", Required, REQUIRED, _), ("popover-open", PopoverOpen, POPOVER_OPEN, _), ("optional", Optional, OPTIONAL_, _), ("valid", Valid, VALID, _), ("invalid", Invalid, INVALID, _), ("in-range", InRange, INRANGE, _), ("out-of-range", OutOfRange, OUTOFRANGE, _), ("default", Default, DEFAULT, _), ("placeholder-shown", PlaceholderShown, PLACEHOLDER_SHOWN, _), ("read-only", ReadOnly, READONLY, _), ("read-write", ReadWrite, READWRITE, _), ("user-valid", UserValid, USER_VALID, _), ("user-invalid", UserInvalid, USER_INVALID, _), ("-moz-meter-optimum", MozMeterOptimum, OPTIMUM, _), ("-moz-meter-sub-optimum", MozMeterSubOptimum, SUB_OPTIMUM, _), ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, SUB_SUB_OPTIMUM, _), ("-moz-first-node", MozFirstNode, _, _), ("-moz-last-node", MozLastNode, _, _), ("-moz-only-whitespace", MozOnlyWhitespace, _, _), ("-moz-native-anonymous", MozNativeAnonymous, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-placeholder", MozPlaceholder, _, _), // Media element pseudo classes ("paused", Paused, PAUSED, _), ("playing", Playing, PAUSED, _), ("seeking", Seeking, SEEKING, _), ("buffering", Buffering, BUFFERING, _), ("stalled", Stalled, STALLED, _), ("muted", Muted, MUTED, _), ("volume-locked", VolumeLocked, _, _), // NOTE(emilio): Pseudo-classes below only depend on document state, and thus // conceptually they should probably be media queries instead. // // However that has a set of trade-offs that might not be worth making. In // particular, such media queries would prevent documents that match them from // sharing user-agent stylesheets with documents that don't. Also, changes between // media query results are more expensive than document state changes. So for now // making them pseudo-classes is probably the right trade-off. ("-moz-is-html", MozIsHTML, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), ("-moz-window-inactive", MozWindowInactive, _, _), ] } } } ================================================ FILE: style/gecko/pseudo_element.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Gecko's definition of a pseudo-element. //! //! Note that a few autogenerated bits of this live in //! `pseudo_element_definition.mako.rs`. If you touch that file, you probably //! need to update the checked-in files for Servo. use crate::gecko_bindings::structs::PseudoStyleType; use crate::properties::longhands::display::computed_value::T as Display; use crate::properties::{ComputedValues, PropertyFlags}; use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl}; use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; use crate::string_cache::Atom; use crate::values::serialize_atom_identifier; use crate::values::AtomIdent; use cssparser::{Parser, ToCss}; use selectors::parser::PseudoElement as PseudoElementTrait; use static_prefs::pref; use std::fmt; use style_traits::ParseError; bitflags! { /// Various pseudo-element flags, see pseudo_elements.toml and anonymous_boxes.toml for the /// meaning. #[derive(Clone, Copy, Default)] pub struct PseudoStyleTypeFlags : u16 { /// No flags const NONE = 0; /// Whether we're enabled in UA sheets. const ENABLED_IN_UA = 1 << 0; /// Whether we're enabled in chrome sheets. const ENABLED_IN_CHROME = 1 << 1; /// Whether we're enabled by a pref. const ENABLED_BY_PREF = 1 << 2; /// Whether we're an anonymous box. const IS_PSEUDO_ELEMENT = 1 << 3; /// Whether we're a CSS2 pseudo-element. const IS_CSS2 = 1 << 4; /// Whether we're an eagerly-cascaded pseudo-element. const IS_EAGER = 1 << 5; /// Whether we can be created by JS const IS_JS_CREATED_NAC = 1 << 6; /// Whether we can be a flex or grid item. const IS_FLEX_OR_GRID_ITEM = 1 << 7; /// Whether we're backed by a real element. const IS_ELEMENT_BACKED = 1 << 8; /// Whether we're a tree-abiding pseudo as per /// https://drafts.csswg.org/css-pseudo-4/#treelike const IS_TREE_ABIDING = 1 << 9; /// Whether we support user-action state pseudo-classes after the pseudo-element. const SUPPORTS_USER_ACTION_STATE = 1 << 10; /// Whether we are an inheriting anon-box. const IS_INHERITING_ANON_BOX = 1 << 11; /// Whether we are a non-inheriting anon box. const IS_NON_INHERITING_ANON_BOX = 1 << 12; /// Combo of the above to cover all anon boxes. const IS_ANON_BOX = Self::IS_INHERITING_ANON_BOX.bits() | Self::IS_NON_INHERITING_ANON_BOX.bits(); /// Whether we're a wrapping anon box. const IS_WRAPPER_ANON_BOX = 1 << 13; /// Whether we parse as an element-backed pseudo-element. const PARSES_AS_ELEMENT_BACKED = 1 << 14; } } include!(concat!( env!("OUT_DIR"), "/gecko/pseudo_element_definition.rs" )); /// The target we are using for parsing pseudo-elements. pub enum Target { /// When parsing a selector, we want to use the full syntax. Selector, /// When parsing the pseudo-element string (from CSSOM), we only accept CusomIdent for named /// view transition pseudo-elements. Cssom, } /// The type to hold the value of ``. /// /// ` = ? | ` /// ` = '*' | ` /// ` = ['.' ]+` /// /// This type should have at least one element. /// If there is no , the first element would be the universal symbol, i.e. '*'. /// In other words, when we match it, ".abc" is the same as "*.abc". /// Note that we also serialize ".abc" as "*.abc". /// /// We use a single ThinVec<> to represent this structure to avoid allocating too much memory for a /// single selectors::parser::Component (max: 24 bytes) and PseudoElement (max: 16 bytes). /// /// https://drafts.csswg.org/css-view-transitions-2/#typedef-pt-name-and-class-selector #[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] pub struct PtNameAndClassSelector(thin_vec::ThinVec); impl PtNameAndClassSelector { /// Constructs a new one from a name. pub fn from_name(name: Atom) -> Self { Self(thin_vec::thin_vec![name]) } /// Returns the name component. pub fn name(&self) -> &Atom { debug_assert!(!self.0.is_empty()); self.0.first().expect("Shouldn't be empty") } /// Returns the classes component. pub fn classes(&self) -> &[Atom] { debug_assert!(!self.0.is_empty()); &self.0[1..] } /// Returns the vector we store. pub fn name_and_classes(&self) -> &thin_vec::ThinVec { &self.0 } /// Parse the pseudo-element tree name and/or class. /// |for_selector| is true if we are parsing the CSS selectors and so need to check the /// universal symbol, i.e. '*', and classes. // Note: We share the same type for both pseudo-element and pseudo-element selector. The // universal symbol (i.e. '*') and `` are used only in the selector (for // matching). pub fn parse<'i, 't>( input: &mut Parser<'i, 't>, target: Target, ) -> Result> { use crate::values::CustomIdent; use cssparser::Token; use style_traits::StyleParseErrorKind; // = '*' | let parse_pt_name = |input: &mut Parser<'i, '_>| { // For pseudo-element string, we don't accept '*'. if matches!(target, Target::Selector) && input.try_parse(|i| i.expect_delim('*')).is_ok() { Ok(atom!("*")) } else { CustomIdent::parse(input, &[]).map(|c| c.0) } }; let name = input.try_parse(parse_pt_name); // Skip for pseudo-element string. if matches!(target, Target::Cssom) { return name.map(Self::from_name); } // = ['.' ]+ let parse_pt_class = |input: &mut Parser<'i, '_>| { // The white space is forbidden: // 1. Between and // 2. Between any of the components of . let location = input.current_source_location(); match input.next_including_whitespace()? { Token::Delim('.') => (), t => return Err(location.new_unexpected_token_error(t.clone())), } // Whitespace is not allowed between '.' and the class name. if let Ok(token) = input.try_parse(|i| i.expect_whitespace()) { return Err(input.new_unexpected_token_error(Token::WhiteSpace(token))); } CustomIdent::parse(input, &[]).map(|c| c.0) }; // If there is no ``, it's fine to have whitespaces before the first '.'. if name.is_err() { input.skip_whitespace(); } let mut classes = thin_vec::ThinVec::new(); while let Ok(class) = input.try_parse(parse_pt_class) { classes.push(class); } // If we don't have ``, we must have ``, per the // syntax: ` ? | `. if name.is_err() && classes.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // Use the universal symbol as the first element to present the part of // `` because they are equivalent (and the serialization is the same). let mut result = thin_vec::thin_vec![name.unwrap_or(atom!("*"))]; result.append(&mut classes); Ok(Self(result)) } } impl ToCss for PtNameAndClassSelector { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { let name = self.name(); if name == &atom!("*") { // serialize_atom_identifier() may serialize "*" as "\*", so we handle it separately. dest.write_char('*')?; } else { serialize_atom_identifier(name, dest)?; } for class in self.classes() { dest.write_char('.')?; serialize_atom_identifier(class, dest)?; } Ok(()) } } impl PseudoElementTrait for PseudoElement { type Impl = SelectorImpl; // ::slotted() should support all tree-abiding pseudo-elements, see // https://drafts.csswg.org/css-scoping/#slotted-pseudo // https://drafts.csswg.org/css-pseudo-4/#treelike #[inline] fn valid_after_slotted(&self) -> bool { self.flags() .intersects(PseudoStyleTypeFlags::IS_TREE_ABIDING) } // ::before/::after should support ::marker, but no others. // https://drafts.csswg.org/css-pseudo-4/#marker-pseudo #[inline] fn valid_after_before_or_after(&self) -> bool { matches!(*self, Self::Marker) } #[inline] fn accepts_state_pseudo_classes(&self) -> bool { // Note: if the pseudo element is a descendants of a pseudo element, `only-child` should be // allowed after it. self.supports_user_action_state() || self.is_in_pseudo_element_tree() } #[inline] fn specificity_count(&self) -> u32 { self.specificity_count() } #[inline] fn is_in_pseudo_element_tree(&self) -> bool { // All the named view transition pseudo-elements are the descendants of a pseudo-element // root. self.is_named_view_transition() } fn parses_as_element_backed(&self) -> bool { self.flags() .intersects(PseudoStyleTypeFlags::PARSES_AS_ELEMENT_BACKED) } /// Whether the current pseudo element is ::before or ::after. #[inline] fn is_before_or_after(&self) -> bool { matches!(*self, PseudoElement::Before | PseudoElement::After) } } impl PseudoElement { /// Whether this pseudo-element is "element-backed", which means that it inherits from its regular /// flat tree parent, which might not be the originating element. #[inline] pub fn is_element_backed(&self) -> bool { self.flags() .intersects(PseudoStyleTypeFlags::IS_ELEMENT_BACKED) } /// Returns the kind of cascade type that a given pseudo is going to use. /// /// In Gecko we only compute ::before and ::after eagerly. We save the rules /// for anonymous boxes separately, so we resolve them as precomputed /// pseudos. /// /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`. pub fn cascade_type(&self) -> PseudoElementCascadeType { if self.is_eager() { debug_assert!(!self.is_anon_box()); return PseudoElementCascadeType::Eager; } if self.is_precomputed() { return PseudoElementCascadeType::Precomputed; } PseudoElementCascadeType::Lazy } /// Returns an index of the pseudo-element. #[inline] pub fn index(&self) -> usize { self.discriminant() as usize } #[inline] fn discriminant(&self) -> u8 { // SAFETY: #[repr(u8) guarantees this, see comments in // https://doc.rust-lang.org/std/mem/fn.discriminant.html unsafe { *(self as *const _ as *const u8) } } /// Whether this pseudo-element is an unknown Webkit-prefixed pseudo-element. #[inline] pub fn is_unknown_webkit_pseudo_element(&self) -> bool { matches!(*self, PseudoElement::UnknownWebkit(..)) } /// Whether this pseudo-element is an anonymous box. #[inline] pub fn is_anon_box(&self) -> bool { self.flags().intersects(PseudoStyleTypeFlags::IS_ANON_BOX) } /// Whether this pseudo-element is eagerly-cascaded. #[inline] pub fn is_eager(&self) -> bool { self.flags().intersects(PseudoStyleTypeFlags::IS_EAGER) } /// Gets the canonical index of this eagerly-cascaded pseudo-element. #[inline] pub fn eager_index(&self) -> usize { EAGER_PSEUDOS .iter() .position(|p| p == self) .expect("Not an eager pseudo") } /// Creates a pseudo-element from an eager index. #[inline] pub fn from_eager_index(i: usize) -> Self { EAGER_PSEUDOS[i].clone() } /// Whether animations for the current pseudo element are stored in the /// parent element. #[inline] pub fn animations_stored_in_parent(&self) -> bool { matches!( *self, Self::Before | Self::After | Self::Marker | Self::Backdrop ) } /// Whether this pseudo-element is the ::before pseudo. #[inline] pub fn is_before(&self) -> bool { *self == PseudoElement::Before } /// Whether this pseudo-element is the ::after pseudo. #[inline] pub fn is_after(&self) -> bool { *self == PseudoElement::After } /// Whether this pseudo-element is the ::marker pseudo. #[inline] pub fn is_marker(&self) -> bool { *self == PseudoElement::Marker } /// Whether this pseudo-element is the ::selection pseudo. #[inline] pub fn is_selection(&self) -> bool { *self == PseudoElement::Selection } /// Whether this pseudo-element is ::first-letter. #[inline] pub fn is_first_letter(&self) -> bool { *self == PseudoElement::FirstLetter } /// Whether this pseudo-element is ::first-line. #[inline] pub fn is_first_line(&self) -> bool { *self == PseudoElement::FirstLine } /// Whether this pseudo-element is lazily-cascaded. #[inline] pub fn is_lazy(&self) -> bool { !self.is_eager() && !self.is_precomputed() } /// The identifier of the highlight this pseudo-element represents. pub fn highlight_name(&self) -> Option<&AtomIdent> { match *self { Self::Highlight(ref name) => Some(name), _ => None, } } /// Whether this pseudo-element is the ::highlight pseudo. pub fn is_highlight(&self) -> bool { matches!(*self, Self::Highlight(_)) } /// Whether this pseudo-element is the ::target-text pseudo. #[inline] pub fn is_target_text(&self) -> bool { *self == PseudoElement::TargetText } /// Whether this is a highlight pseudo-element that is styled lazily during /// painting rather than during the restyle traversal. These pseudos need /// explicit repaint triggering when their styles change. #[inline] pub fn is_lazy_painted_highlight_pseudo(&self) -> bool { self.is_selection() || self.is_highlight() || self.is_target_text() } /// Whether this pseudo-element is a named view transition pseudo-element. pub fn is_named_view_transition(&self) -> bool { matches!( *self, Self::ViewTransitionGroup(..) | Self::ViewTransitionImagePair(..) | Self::ViewTransitionOld(..) | Self::ViewTransitionNew(..) ) } /// The count we contribute to the specificity from this pseudo-element. pub fn specificity_count(&self) -> u32 { match *self { Self::ViewTransitionGroup(ref name_and_class) | Self::ViewTransitionImagePair(ref name_and_class) | Self::ViewTransitionOld(ref name_and_class) | Self::ViewTransitionNew(ref name_and_class) => { // The specificity of a named view transition pseudo-element selector with either: // 1. a with a ; or // 2. a with at least one , // is equivalent to a type selector. // // The specificity of a named view transition pseudo-element selector with a `*` // argument and with an empty is zero. // https://drafts.csswg.org/css-view-transitions-2/#pseudo-element-class-additions (name_and_class.name() != &atom!("*") || !name_and_class.classes().is_empty()) as u32 }, _ => 1, } } /// Whether this pseudo-element supports user action selectors. pub fn supports_user_action_state(&self) -> bool { self.flags() .intersects(PseudoStyleTypeFlags::SUPPORTS_USER_ACTION_STATE) } /// Whether this pseudo-element is enabled for all content. pub fn enabled_in_content(&self) -> bool { Self::type_enabled_in_content(self.pseudo_type()) } /// Whether this pseudo is enabled explicitly in UA sheets. pub fn enabled_in_ua_sheets(&self) -> bool { self.flags().intersects(PseudoStyleTypeFlags::ENABLED_IN_UA) } /// Whether this pseudo is enabled explicitly in chrome sheets. pub fn enabled_in_chrome(&self) -> bool { self.flags() .intersects(PseudoStyleTypeFlags::ENABLED_IN_CHROME) } /// Whether this pseudo-element skips flex/grid container display-based /// fixup. #[inline] pub fn skip_item_display_fixup(&self) -> bool { !self .flags() .intersects(PseudoStyleTypeFlags::IS_FLEX_OR_GRID_ITEM) } /// Whether this pseudo-element is precomputed. #[inline] pub fn is_precomputed(&self) -> bool { self.is_anon_box() } /// Property flag that properties must have to apply to this pseudo-element. #[inline] pub fn property_restriction(&self) -> Option { Some(match *self { PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER, PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE, PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER, PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE, PseudoElement::Marker => PropertyFlags::APPLIES_TO_MARKER, _ => return None, }) } /// Whether this pseudo-element should actually exist if it has /// the given styles. pub fn should_exist(&self, style: &ComputedValues) -> bool { debug_assert!(self.is_eager()); if style.get_box().clone_display() == Display::None { return false; } if self.is_before_or_after() && style.ineffective_content_property() { return false; } true } /// Parse the pseudo-element string without the check of enabled state. This may includes /// all possible PseudoElement, including tree pseudo-elements and anonymous box. // TODO: Bug 1845712. Merge this with the pseudo element part in parse_one_simple_selector(). pub fn parse_ignore_enabled_state<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { use crate::gecko::selector_parser; use cssparser::Token; use selectors::parser::{is_css2_pseudo_element, SelectorParseErrorKind}; use style_traits::StyleParseErrorKind; // The pseudo-element string should start with ':'. input.expect_colon()?; let location = input.current_source_location(); let next = input.next_including_whitespace()?; if !matches!(next, Token::Colon) { // Parse a CSS2 pseudo-element. let name = match next { Token::Ident(name) if is_css2_pseudo_element(&name) => name, _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), }; return PseudoElement::from_slice(&name, false).ok_or(location.new_custom_error( SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()), )); } // Now we have double colons, so check the following tokens. match input.next_including_whitespace()?.clone() { Token::Ident(name) => { // We don't need to parse unknown ::-webkit-* pseudo-elements in this function. PseudoElement::from_slice(&name, false).ok_or(input.new_custom_error( SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), )) }, Token::Function(name) => { // Note: ::slotted() and ::part() are not accepted in getComputedStyle(). // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle input.parse_nested_block(|input| { selector_parser::parse_functional_pseudo_element_with_name( &name, input, Target::Cssom, ) }) }, t => return Err(input.new_unexpected_token_error(t)), } } /// Returns true if this pseudo-element matches its selector. pub fn matches_named_view_transition_pseudo_element( &self, selector: &Self, element: &super::wrapper::GeckoElement, ) -> bool { use crate::gecko_bindings::bindings; match (self, selector) { ( &Self::ViewTransitionGroup(ref name), &Self::ViewTransitionGroup(ref s_name_class), ) | ( &Self::ViewTransitionImagePair(ref name), &Self::ViewTransitionImagePair(ref s_name_class), ) | (&Self::ViewTransitionOld(ref name), &Self::ViewTransitionOld(ref s_name_class)) | (&Self::ViewTransitionNew(ref name), &Self::ViewTransitionNew(ref s_name_class)) => { // Named view transition pseudos accept the universal selector as the name, so we // check it first. // https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo let s_name = s_name_class.name(); if s_name != name.name() && s_name != &atom!("*") { return false; } // We have to check class list only when the name is matched and there are one or // more s. s_name_class.classes().is_empty() || unsafe { bindings::Gecko_MatchViewTransitionClass( element.0, s_name_class.name_and_classes(), ) } }, _ => false, } } } ================================================ FILE: style/gecko/pseudo_element_definition.mako.rs ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::derives::*; type AtomThinVec = thin_vec::ThinVec; /// Gecko's pseudo-element definition. /// /// We intentionally double-box legacy ::-moz-tree pseudo-elements to keep the /// size of PseudoElement (and thus selector components) small. #[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] #[repr(u8)] pub enum PseudoElement { % for pseudo in PSEUDOS: /// ${pseudo.name} % if pseudo.argument: ${pseudo.capitalized}(${pseudo.argument}), % else: ${pseudo.capitalized}, % endif % endfor /// ::-webkit-* that we don't recognize /// https://github.com/whatwg/compat/issues/103 UnknownWebkit(Atom), } <% EAGER_PSEUDOS = list(filter(lambda p: getattr(p, 'is_eager', False), PSEUDOS)) %> /// The number of eager pseudo-elements. pub const EAGER_PSEUDO_COUNT: usize = ${len(EAGER_PSEUDOS)}; /// The number of all pseudo-elements. pub const PSEUDO_COUNT: usize = ${len(PSEUDOS)}; /// The list of eager pseudos. pub const EAGER_PSEUDOS: [PseudoElement; EAGER_PSEUDO_COUNT] = [ % for eager_pseudo in EAGER_PSEUDOS: PseudoElement::${eager_pseudo.capitalized}, % endfor ]; <%def name="pseudo_element_variant(pseudo, arg='..')">\ PseudoElement::${pseudo.capitalized}${"({})".format(arg) if pseudo.argument else ""}\ impl PseudoElement { /// Whether this pseudo-element is tree pseudo-element. #[inline] pub fn is_tree_pseudo_element(&self) -> bool { match *self { % for pseudo in PSEUDOS: % if pseudo.name.startswith("-moz-tree-"): ${pseudo_element_variant(pseudo)} => true, % endif % endfor _ => false, } } #[inline] fn flags_for_index(i: usize) -> PseudoStyleTypeFlags { static FLAGS: [PseudoStyleTypeFlags; PSEUDO_COUNT + 1] = [ % for pseudo in PSEUDOS: PseudoStyleTypeFlags::from_bits_truncate(${' | '.join(f"PseudoStyleTypeFlags::{flag}.bits()" for flag in pseudo.flags())}), % endfor PseudoStyleTypeFlags::NONE, // :-webkit-* ]; FLAGS[i] } /// Gets the flags associated to this pseudo-element or anon box. #[inline] pub fn flags(&self) -> PseudoStyleTypeFlags { Self::flags_for_index(self.index()) } /// If this pseudo is pref-gated, returns the pref value, otherwise false. pub fn type_enabled_in_content(ty: PseudoStyleType) -> bool { let flags = Self::flags_for_index(ty as usize); // Common path, not enabled explicitly in UA sheets (note that chrome implies UA), and not // pref-gated. if !flags.intersects(PseudoStyleTypeFlags::ENABLED_IN_UA | PseudoStyleTypeFlags::ENABLED_BY_PREF) { return true; } if !flags.intersects(PseudoStyleTypeFlags::ENABLED_BY_PREF) { return false; } match ty { % for pseudo in PSEUDOS: % if pseudo.pref: PseudoStyleType::${pseudo.capitalized} => pref!("${pseudo.pref}"), % endif % endfor _ => false, } } /// Construct a pseudo-element from a `PseudoStyleType`. #[inline] pub fn from_pseudo_type(type_: PseudoStyleType, functional_pseudo_parameter: Option) -> Option { match type_ { % for pseudo in PSEUDOS: % if not pseudo.argument: PseudoStyleType::${pseudo.capitalized} => { debug_assert!(functional_pseudo_parameter.is_none()); Some(${pseudo_element_variant(pseudo)}) }, % elif pseudo.argument == "AtomThinVec": PseudoStyleType::${pseudo.capitalized} => { debug_assert!(functional_pseudo_parameter.is_none()); Some(${pseudo_element_variant(pseudo, "Default::default()")}) }, % elif pseudo.argument == "PtNameAndClassSelector": PseudoStyleType::${pseudo.capitalized} => functional_pseudo_parameter.map(|p| { PseudoElement::${pseudo.capitalized}(PtNameAndClassSelector::from_name(p.0)) }), % else: <% assert pseudo.argument == "AtomIdent", f"Unhandled argument type {pseudo.argument}" %> PseudoStyleType::${pseudo.capitalized} => { functional_pseudo_parameter.map(PseudoElement::${pseudo.capitalized}) }, % endif % endfor _ => None, } } /// Construct a `PseudoStyleType`. #[inline] pub fn pseudo_type(&self) -> PseudoStyleType { // SAFETY: PseudoStyleType has the same variants as PseudoElement unsafe { std::mem::transmute::(self.discriminant()) } } /// Returns the relevant PseudoStyleType, and an atom as an argument, if any. /// FIXME: we probably have to return the arguments of -moz-tree. However, they are multiple /// names, so we skip them for now (until we really need them). #[inline] pub fn pseudo_type_and_argument(&self) -> (PseudoStyleType, Option<&Atom>) { let ty = self.pseudo_type(); let arg = match *self { % for pseudo in PSEUDOS: % if pseudo.argument == "PtNameAndClassSelector": PseudoElement::${pseudo.capitalized}(ref val) => Some(val.name()), % elif pseudo.argument == "AtomIdent": PseudoElement::${pseudo.capitalized}(ref val) => Some(&val.0), % endif % endfor _ => None, }; (ty, arg) } /// Get the argument list of a tree pseudo-element. #[inline] pub fn tree_pseudo_args(&self) -> &[Atom] { match *self { % for pseudo in PSEUDOS: % if pseudo.name.startswith("-moz-tree-"): PseudoElement::${pseudo.capitalized}(ref args) => &args, % endif % endfor _ => &[], } } /// Constructs a pseudo-element from a string of text. /// /// Returns `None` if the pseudo-element is not recognised. #[inline] pub fn from_slice(name: &str, allow_unkown_webkit: bool) -> Option { // We don't need to support tree pseudos because functional // pseudo-elements needs arguments, and thus should be created // via other methods. cssparser::ascii_case_insensitive_phf_map! { pseudo -> PseudoElement = { % for pseudo in PSEUDOS: % if not pseudo.argument: "${pseudo.name}" => ${pseudo_element_variant(pseudo)}, % endif % endfor // Alias some legacy prefixed pseudos to their standardized name at parse time: "-moz-selection" => PseudoElement::Selection, "-moz-placeholder" => PseudoElement::Placeholder, "-moz-list-bullet" => PseudoElement::Marker, "-moz-list-number" => PseudoElement::Marker, } } if let Some(p) = pseudo::get(name) { return Some(p.clone()); } if starts_with_ignore_ascii_case(name, "-moz-tree-") { return PseudoElement::tree_pseudo_element(name, Default::default()) } const WEBKIT_PREFIX: &str = "-webkit-"; if allow_unkown_webkit && starts_with_ignore_ascii_case(name, WEBKIT_PREFIX) { let part = string_as_ascii_lowercase(&name[WEBKIT_PREFIX.len()..]); return Some(PseudoElement::UnknownWebkit(part.into())); } None } /// Constructs a tree pseudo-element from the given name and arguments. /// "name" must start with "-moz-tree-". /// /// Returns `None` if the pseudo-element is not recognized. #[inline] pub fn tree_pseudo_element(name: &str, args: thin_vec::ThinVec) -> Option { debug_assert!(starts_with_ignore_ascii_case(name, "-moz-tree-")); let tree_part = &name[10..]; % for pseudo in PSEUDOS: % if pseudo.name.startswith("-moz-tree-"): if tree_part.eq_ignore_ascii_case("${pseudo.name[10:]}") { return Some(${pseudo_element_variant(pseudo, "args")}); } % endif % endfor None } /// Returns true if this pseudo-element matches the given selector. pub fn matches( &self, pseudo_selector: &PseudoElement, element: &super::wrapper::GeckoElement, ) -> bool { if *self == *pseudo_selector { return true; } if std::mem::discriminant(self) != std::mem::discriminant(pseudo_selector) { return false; } // Check named view transition pseudo-elements. self.matches_named_view_transition_pseudo_element(pseudo_selector, element) } } impl ToCss for PseudoElement { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { match *self { % for pseudo in PSEUDOS: % if pseudo.argument == "AtomThinVec": ${pseudo_element_variant(pseudo, "ref args")} => { dest.write_str("::${pseudo.name}")?; let mut iter = args.iter(); if let Some(first) = iter.next() { dest.write_char('(')?; serialize_atom_identifier(&first, dest)?; for item in iter { dest.write_str(", ")?; serialize_atom_identifier(item, dest)?; } dest.write_char(')')?; } Ok(()) }, % elif pseudo.argument: PseudoElement::${pseudo.capitalized}(ref arg) => { dest.write_str("::${pseudo.name}(")?; arg.to_css(dest)?; dest.write_char(')') } % else: ${pseudo_element_variant(pseudo)} => dest.write_str("::${pseudo.name}"), % endif % endfor PseudoElement::UnknownWebkit(ref atom) => { dest.write_str("::-webkit-")?; serialize_atom_identifier(atom, dest) }, } } } ================================================ FILE: style/gecko/pseudo_elements.py ================================================ #!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import os import sys import toml THIS_DIR = os.path.dirname(__file__) sys.path.insert(0, os.path.join(os.path.dirname(THIS_DIR), "properties")) import data class Pseudo(object): def __init__(self, name, argument, enabled_in, pref): self.name = name self.camel = data.to_camel_case(name) self.capitalized = self.camel[0].upper() + self.camel[1:] self.argument = argument self.enabled_in = enabled_in self.pref = pref def is_pseudo_element(self): return isinstance(self, PseudoElement) def is_anon_box(self): return isinstance(self, AnonBox) def is_non_inheriting_anon_box(self): return isinstance(self, AnonBox) and not self.inherits def is_inheriting_anon_box(self): return isinstance(self, AnonBox) and self.inherits def flags(self): flags = [] if self.enabled_in == "ua": flags.append("ENABLED_IN_UA") elif self.enabled_in == "chrome": flags.append("ENABLED_IN_UA") flags.append("ENABLED_IN_CHROME") if self.pref: flags.append("ENABLED_BY_PREF") if isinstance(self, PseudoElement): flags.append("IS_PSEUDO_ELEMENT") if self.is_css2: flags.append("IS_CSS2") if self.is_eager: flags.append("IS_EAGER") if self.is_js_created_nac: flags.append("IS_JS_CREATED_NAC") if self.is_flex_or_grid_item: flags.append("IS_FLEX_OR_GRID_ITEM") if self.is_element_backed: flags.append("IS_ELEMENT_BACKED") if self.is_tree_abiding: flags.append("IS_TREE_ABIDING") if self.parses_as_element_backed: flags.append("PARSES_AS_ELEMENT_BACKED") if self.supports_user_action_state: flags.append("SUPPORTS_USER_ACTION_STATE") if isinstance(self, AnonBox): if self.inherits: flags.append("IS_INHERITING_ANON_BOX") else: flags.append("IS_NON_INHERITING_ANON_BOX") if self.wrapper: flags.append("IS_WRAPPER_ANON_BOX") return flags class AnonBox(Pseudo): def __init__(self, name, wrapper=False, inherits=True): super().__init__(name, argument=None, enabled_in="ua", pref=None) self.wrapper = wrapper self.inherits = inherits class PseudoElement(Pseudo): def __init__( self, name, enabled_in="content", is_css2=False, is_eager=False, is_js_created_nac=False, is_flex_or_grid_item=False, is_element_backed=False, is_tree_abiding=False, parses_as_element_backed=None, supports_user_action_state=False, pref=None, argument=None, ): super().__init__(name, argument=argument, enabled_in=enabled_in, pref=pref) self.is_css2 = is_css2 self.is_eager = is_eager self.is_js_created_nac = is_js_created_nac self.is_flex_or_grid_item = is_flex_or_grid_item self.is_element_backed = is_element_backed self.is_tree_abiding = is_element_backed or is_tree_abiding self.parses_as_element_backed = ( parses_as_element_backed if parses_as_element_backed is not None else is_element_backed ) self.supports_user_action_state = supports_user_action_state class PseudoElementData: def __init__(self): this_dir = os.path.dirname(__file__) pseudo_elements_toml = os.path.join(this_dir, "pseudo_elements.toml") anon_boxes_toml = os.path.join(this_dir, "anon_boxes.toml") self.anon_boxes = sorted( ( AnonBox(name, **val) for name, val in toml.loads(open(anon_boxes_toml).read()).items() ), key=lambda n: n.inherits, ) self.pseudo_elements = [ PseudoElement(name, **val) for name, val in toml.loads(open(pseudo_elements_toml).read()).items() ] self.path_dependencies = [pseudo_elements_toml, anon_boxes_toml] def all_pseudos(self): return self.anon_boxes + self.pseudo_elements ================================================ FILE: style/gecko/pseudo_elements.toml ================================================ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This file contains the list of support CSS pseudo-elements and some flags. # # Each pseudo element has its own section, with the following keys: # # enabled_in - Same semantics as longhands.toml # is_css2 - Default false, whether this is a css2 pseudo-element, and can be # parsed only with one colon. # is_eager - Whether the pseudo-element is eagerly cascaded by the style # system during traversal. This should generally only be used for # pseudo-elements that apply to most elements. # is_js_created_nac - True if this pseudo-element can be created by JS. # is_flex_or_grid_item - True if display item fixup for flex / grid should # happen for this pseudo # is_element_backed - whether this is an element-backed pseudo-element # https://drafts.csswg.org/css-pseudo-4/#element-like # supports_user_action_state - true if this pseudo supports :hover etc. # argument - If provided, the argument this functional pseudo takes. # pref - Same semantics as gecko_pref in longhands.toml # # When adding a pseudo-element, you also need to add the relevant atom to # StaticAtoms.py, with the camel-case name of the pseudo-element. [after] is_css2 = true is_eager = true is_flex_or_grid_item = true is_tree_abiding = true [before] is_css2 = true is_eager = true is_flex_or_grid_item = true is_tree_abiding = true [marker] is_tree_abiding = true [backdrop] [cue] is_js_created_nac = true [first-letter] is_css2 = true is_eager = true [first-line] is_css2 = true is_eager = true [highlight] argument = "AtomIdent" [selection] [target-text] [view-transition] enabled_in = "ua" pref = "dom.viewTransitions.enabled" [view-transition-group] enabled_in = "ua" pref = "dom.viewTransitions.enabled" argument = "PtNameAndClassSelector" is_element_backed = true [view-transition-image-pair] enabled_in = "ua" pref = "dom.viewTransitions.enabled" is_element_backed = true argument = "PtNameAndClassSelector" [view-transition-old] enabled_in = "ua" pref = "dom.viewTransitions.enabled" is_element_backed = true argument = "PtNameAndClassSelector" [view-transition-new] enabled_in = "ua" pref = "dom.viewTransitions.enabled" is_element_backed = true argument = "PtNameAndClassSelector" # The internal implementation usage for View transition to create the snapshot # containing block concept. [-moz-snapshot-containing-block] enabled_in = "ua" [picker] enabled_in = "ua" pref = "dom.select.customizable_select.enabled" argument = "AtomIdent" is_element_backed = true # HTML5 Forms pseudo elements [-moz-number-spin-box] supports_user_action_state = true enabled_in = "chrome" [-moz-number-spin-down] supports_user_action_state = true enabled_in = "chrome" [-moz-number-spin-up] supports_user_action_state = true enabled_in = "chrome" [-moz-search-clear-button] supports_user_action_state = true enabled_in = "chrome" # The selected label of a [picker-icon] enabled_in = "chrome" pref = "dom.select.customizable_select.enabled" is_element_backed = true # We represent ::picker-icon with a real element, but per spec it should parse # as a regular generated content pseudo-element. parses_as_element_backed = false [-moz-progress-bar] supports_user_action_state = true [-moz-range-track] supports_user_action_state = true [-moz-range-progress] supports_user_action_state = true [-moz-range-thumb] supports_user_action_state = true [-moz-meter-bar] supports_user_action_state = true [placeholder] supports_user_action_state = true is_element_backed = true parses_as_element_backed = false [-moz-color-swatch] supports_user_action_state = true # The root of the text value anonymous content inside an or