├── .cargo └── config.toml ├── .github └── workflows │ ├── release-crates.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bench.js ├── c ├── Cargo.toml ├── build.rs ├── cbindgen.toml ├── lightningcss.h ├── src │ └── lib.rs └── test.c ├── cli ├── .gitignore ├── lightningcss └── postinstall.js ├── derive ├── Cargo.toml └── src │ ├── lib.rs │ ├── parse.rs │ ├── to_css.rs │ └── visit.rs ├── examples ├── custom_at_rule.rs ├── schema.rs └── serialize.rs ├── napi ├── Cargo.toml └── src │ ├── at_rule_parser.rs │ ├── lib.rs │ ├── threadsafe_function.rs │ ├── transformer.rs │ └── utils.rs ├── node ├── Cargo.toml ├── ast.d.ts ├── browserslistToTargets.js ├── build.rs ├── composeVisitors.js ├── flags.js ├── index.d.ts ├── index.js ├── index.mjs ├── src │ └── lib.rs ├── targets.d.ts ├── test │ ├── bundle.test.mjs │ ├── composeVisitors.test.mjs │ ├── customAtRules.mjs │ ├── transform.test.mjs │ └── visitor.test.mjs └── tsconfig.json ├── package.json ├── patches ├── @babel+types+7.26.3.patch └── json-schema-to-typescript+11.0.5.patch ├── rust-toolchain.toml ├── rustfmt.toml ├── scripts ├── build-ast.js ├── build-flow.js ├── build-npm.js ├── build-prefixes.js ├── build-wasm.js └── build.js ├── selectors ├── Cargo.toml ├── LICENSE ├── README.md ├── attr.rs ├── bloom.rs ├── build.rs ├── builder.rs ├── context.rs ├── lib.rs ├── matching.rs ├── nth_index_cache.rs ├── parser.rs ├── serialization.rs ├── sink.rs ├── tree.rs └── visitor.rs ├── src ├── bundler.rs ├── compat.rs ├── context.rs ├── css_modules.rs ├── declaration.rs ├── dependencies.rs ├── error.rs ├── lib.rs ├── logical.rs ├── macros.rs ├── main.rs ├── media_query.rs ├── parser.rs ├── prefixes.rs ├── printer.rs ├── properties │ ├── align.rs │ ├── animation.rs │ ├── background.rs │ ├── border.rs │ ├── border_image.rs │ ├── border_radius.rs │ ├── box_shadow.rs │ ├── contain.rs │ ├── css_modules.rs │ ├── custom.rs │ ├── display.rs │ ├── effects.rs │ ├── flex.rs │ ├── font.rs │ ├── grid.rs │ ├── list.rs │ ├── margin_padding.rs │ ├── masking.rs │ ├── mod.rs │ ├── outline.rs │ ├── overflow.rs │ ├── position.rs │ ├── prefix_handler.rs │ ├── size.rs │ ├── svg.rs │ ├── text.rs │ ├── transform.rs │ ├── transition.rs │ └── ui.rs ├── rules │ ├── container.rs │ ├── counter_style.rs │ ├── custom_media.rs │ ├── document.rs │ ├── font_face.rs │ ├── font_feature_values.rs │ ├── font_palette_values.rs │ ├── import.rs │ ├── keyframes.rs │ ├── layer.rs │ ├── media.rs │ ├── mod.rs │ ├── namespace.rs │ ├── nesting.rs │ ├── page.rs │ ├── property.rs │ ├── scope.rs │ ├── starting_style.rs │ ├── style.rs │ ├── supports.rs │ ├── unknown.rs │ ├── view_transition.rs │ └── viewport.rs ├── selector.rs ├── serialization.rs ├── stylesheet.rs ├── targets.rs ├── traits.rs ├── values │ ├── alpha.rs │ ├── angle.rs │ ├── calc.rs │ ├── color.rs │ ├── easing.rs │ ├── gradient.rs │ ├── ident.rs │ ├── image.rs │ ├── length.rs │ ├── mod.rs │ ├── number.rs │ ├── percentage.rs │ ├── position.rs │ ├── ratio.rs │ ├── rect.rs │ ├── resolution.rs │ ├── shape.rs │ ├── size.rs │ ├── string.rs │ ├── syntax.rs │ ├── time.rs │ └── url.rs ├── vendor_prefix.rs └── visitor.rs ├── static-self-derive ├── Cargo.toml └── src │ ├── into_owned.rs │ └── lib.rs ├── static-self ├── Cargo.toml └── src │ └── lib.rs ├── test-integration.mjs ├── test.js ├── tests ├── cli_integration_tests.rs ├── test_cssom.rs ├── test_custom_parser.rs ├── test_serde.rs └── testdata │ ├── a.css │ ├── apply.css │ ├── b.css │ ├── baz.css │ ├── foo.css │ ├── hello │ └── world.css │ └── mixin.css ├── wasm ├── .gitignore ├── async.mjs ├── import.meta.url-polyfill.js ├── index.mjs └── wasm-node.mjs ├── website ├── .posthtmlrc ├── bundling.html ├── crush.svg ├── css-modules.html ├── docs.css ├── docs.html ├── docs.js ├── favicon.svg ├── include │ └── layout.html ├── index.html ├── lightspeed.svg ├── metal.jpeg ├── minification.html ├── og.jpeg ├── pages │ ├── bundling.md │ ├── css-modules.md │ ├── docs.md │ ├── minification.md │ ├── transforms.md │ └── transpilation.md ├── playground │ ├── index.html │ └── playground.js ├── synthwave.css ├── transforms.html └── transpilation.html └── yarn.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_env = "gnu")'] 2 | rustflags = ["-C", "link-args=-Wl,-z,nodelete"] 3 | 4 | [target.aarch64-unknown-linux-gnu] 5 | linker = "aarch64-linux-gnu-gcc" 6 | 7 | [target.armv7-unknown-linux-gnueabihf] 8 | linker = "arm-linux-gnueabihf-gcc" 9 | 10 | [target.aarch64-unknown-linux-musl] 11 | linker = "aarch64-linux-musl-gcc" 12 | rustflags = ["-C", "target-feature=-crt-static"] 13 | 14 | [target.wasm32-unknown-unknown] 15 | rustflags = ["-C", "link-arg=--export-table"] 16 | 17 | # Statically link Visual Studio redistributables on Windows builds 18 | [target.x86_64-pc-windows-msvc] 19 | rustflags = ["-C", "target-feature=+crt-static"] 20 | [target.aarch64-pc-windows-msvc] 21 | rustflags = ["-C", "target-feature=+crt-static"] 22 | -------------------------------------------------------------------------------- /.github/workflows/release-crates.yml: -------------------------------------------------------------------------------- 1 | name: release-crates 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | release-crates: 7 | runs-on: ubuntu-latest 8 | name: Release Rust crate 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: bahmutov/npm-install@v1.8.32 12 | - name: Install Rust 13 | uses: dtolnay/rust-toolchain@stable 14 | - run: cargo login ${CRATES_IO_TOKEN} 15 | env: 16 | CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 17 | - run: | 18 | cargo install cargo-workspaces 19 | cargo workspaces publish --no-remove-dev-deps --from-git -y 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | env: 13 | CARGO_TERM_COLOR: always 14 | RUST_BACKTRACE: full 15 | RUSTFLAGS: -D warnings 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: dtolnay/rust-toolchain@stable 19 | - uses: Swatinem/rust-cache@v2 20 | - run: cargo fmt 21 | - run: cargo test --all-features 22 | 23 | test-js: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 18 30 | - uses: bahmutov/npm-install@v1.8.32 31 | - uses: dtolnay/rust-toolchain@stable 32 | - uses: Swatinem/rust-cache@v2 33 | - run: yarn build 34 | - run: yarn test 35 | - run: yarn tsc 36 | 37 | test-wasm: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: 18 44 | - uses: bahmutov/npm-install@v1.8.32 45 | - uses: dtolnay/rust-toolchain@stable 46 | with: 47 | targets: wasm32-unknown-unknown 48 | - name: Setup rust target 49 | run: rustup target add wasm32-unknown-unknown 50 | - uses: Swatinem/rust-cache@v2 51 | - name: Install wasm-opt 52 | run: | 53 | curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz 54 | tar -xf binaryen-version_111-x86_64-linux.tar.gz 55 | - name: Build wasm 56 | run: | 57 | export PATH="$PATH:./binaryen-version_111/bin" 58 | yarn wasm:build-release 59 | - run: TEST_WASM=node yarn test 60 | - run: TEST_WASM=browser yarn test 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.node 3 | node_modules/ 4 | target/ 5 | pkg/ 6 | dist/ 7 | .parcel-cache 8 | node/*.flow 9 | artifacts 10 | npm 11 | node/ast.json 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "endOfLine": "lf", 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome, we really appreciate if you're considering to contribute, the joint effort of our contributors make projects like this possible! 4 | 5 | The goal of this document is to provide guidance on how you can get involved. 6 | 7 | ## Getting started with bug fixing 8 | 9 | In order to make it easier to get familiar with the codebase we labeled simpler issues using [Good First Issue](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) and [Help Wanted](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22). 10 | 11 | Before starting make sure you have the following requirements installed: [git](https://git-scm.com), [Node](https://nodejs.org), [Yarn](https://yarnpkg.com) and [Rust](https://www.rust-lang.org/tools/install). 12 | 13 | The process starts by [forking](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the project and setup a new branch to work in. It's important that the changes are made in separated branches in order to ensure a pull request only includes the commits related to a bug or feature. 14 | 15 | Clone the forked repository locally and install the dependencies: 16 | 17 | ```sh 18 | git clone https://github.com/USERNAME/lightningcss.git 19 | cd lightningcss 20 | yarn install 21 | ``` 22 | 23 | ## Testing 24 | 25 | In order to test, you first need to build the core package: 26 | 27 | ```sh 28 | yarn build 29 | ``` 30 | 31 | Then you can run the tests: 32 | 33 | ```sh 34 | yarn test # js tests 35 | cargo test # rust tests 36 | ``` 37 | 38 | ## Building 39 | 40 | There are different build targets available, with "release" being a production build: 41 | 42 | ```sh 43 | yarn build 44 | yarn build-release 45 | 46 | yarn wasm:build 47 | yarn wasm:build-release 48 | ``` 49 | 50 | ## Website 51 | 52 | The website is built using [Parcel](https://parceljs.org). You can start the development server by running: 53 | 54 | ```sh 55 | yarn website:start 56 | ``` 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "node", 4 | "napi", 5 | "selectors", 6 | "c", 7 | "derive", 8 | "static-self", 9 | "static-self-derive", 10 | ] 11 | 12 | [package] 13 | authors = ["Devon Govett "] 14 | name = "lightningcss" 15 | version = "1.0.0-alpha.66" 16 | description = "A CSS parser, transformer, and minifier" 17 | license = "MPL-2.0" 18 | edition = "2021" 19 | keywords = ["CSS", "minifier", "Parcel"] 20 | repository = "https://github.com/parcel-bundler/lightningcss" 21 | 22 | [package.metadata.docs.rs] 23 | all-features = true 24 | rustdoc-args = ["--cfg", "docsrs"] 25 | 26 | [[bin]] 27 | name = "lightningcss" 28 | path = "src/main.rs" 29 | required-features = ["cli"] 30 | 31 | [lib] 32 | name = "lightningcss" 33 | path = "src/lib.rs" 34 | crate-type = ["rlib"] 35 | 36 | [features] 37 | default = ["bundler", "nodejs", "sourcemap"] 38 | browserslist = ["browserslist-rs"] 39 | bundler = ["dashmap", "sourcemap", "rayon"] 40 | cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"] 41 | jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"] 42 | nodejs = ["dep:serde"] 43 | serde = [ 44 | "dep:serde", 45 | "smallvec/serde", 46 | "cssparser/serde", 47 | "parcel_selectors/serde", 48 | "into_owned", 49 | ] 50 | sourcemap = ["parcel_sourcemap"] 51 | visitor = [] 52 | into_owned = [ 53 | "static-self", 54 | "static-self/smallvec", 55 | "static-self/indexmap", 56 | "parcel_selectors/into_owned", 57 | ] 58 | substitute_variables = ["visitor", "into_owned"] 59 | 60 | [dependencies] 61 | serde = { version = "1.0.201", features = ["derive"], optional = true } 62 | cssparser = "0.33.0" 63 | cssparser-color = "0.1.0" 64 | parcel_selectors = { version = "0.28.2", path = "./selectors" } 65 | itertools = "0.10.1" 66 | smallvec = { version = "1.7.0", features = ["union"] } 67 | bitflags = "2.2.1" 68 | parcel_sourcemap = { version = "2.1.1", features = ["json"], optional = true } 69 | data-encoding = "2.3.2" 70 | lazy_static = "1.4.0" 71 | const-str = "0.3.1" 72 | pathdiff = "0.2.1" 73 | ahash = "0.8.7" 74 | paste = "1.0.12" 75 | indexmap = { version = "2.2.6", features = ["serde"] } 76 | # CLI deps 77 | atty = { version = "0.2", optional = true } 78 | clap = { version = "3.0.6", features = ["derive"], optional = true } 79 | browserslist-rs = { version = "0.18.1", optional = true } 80 | rayon = { version = "1.5.1", optional = true } 81 | dashmap = { version = "5.0.0", optional = true } 82 | serde_json = { version = "1.0.78", optional = true } 83 | lightningcss-derive = { version = "=1.0.0-alpha.43", path = "./derive" } 84 | schemars = { version = "0.8.19", features = ["smallvec", "indexmap2"], optional = true } 85 | static-self = { version = "0.1.2", path = "static-self", optional = true } 86 | 87 | [target.'cfg(target_os = "macos")'.dependencies] 88 | jemallocator = { version = "0.3.2", features = [ 89 | "disable_initial_exec_tls", 90 | ], optional = true } 91 | 92 | [target.'cfg(target_arch = "wasm32")'.dependencies] 93 | getrandom = { version = "0.2", features = ["custom"], default-features = false } 94 | 95 | [dev-dependencies] 96 | indoc = "1.0.3" 97 | assert_cmd = "2.0" 98 | assert_fs = "1.0" 99 | predicates = "2.1" 100 | serde_json = "1" 101 | pretty_assertions = "1.4.0" 102 | 103 | [[test]] 104 | name = "cli_integration_tests" 105 | path = "tests/cli_integration_tests.rs" 106 | required-features = ["cli"] 107 | 108 | [[example]] 109 | name = "custom_at_rule" 110 | required-features = ["visitor"] 111 | 112 | [[example]] 113 | name = "serialize" 114 | required-features = ["serde"] 115 | 116 | [profile.release] 117 | lto = true 118 | codegen-units = 1 119 | panic = 'abort' 120 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | const css = require('./'); 2 | const cssnano = require('cssnano'); 3 | const postcss = require('postcss'); 4 | const esbuild = require('esbuild'); 5 | 6 | let opts = { 7 | filename: process.argv[process.argv.length - 1], 8 | code: require('fs').readFileSync(process.argv[process.argv.length - 1]), 9 | minify: true, 10 | // source_map: true, 11 | targets: { 12 | chrome: 95 << 16 13 | } 14 | }; 15 | 16 | async function run() { 17 | await doCssNano(); 18 | 19 | console.time('esbuild'); 20 | let r = await esbuild.transform(opts.code.toString(), { 21 | sourcefile: opts.filename, 22 | loader: 'css', 23 | minify: true 24 | }); 25 | console.timeEnd('esbuild'); 26 | console.log(r.code.length + ' bytes'); 27 | console.log(''); 28 | 29 | console.time('lightningcss'); 30 | let res = css.transform(opts); 31 | console.timeEnd('lightningcss'); 32 | console.log(res.code.length + ' bytes'); 33 | } 34 | 35 | async function doCssNano() { 36 | console.time('cssnano'); 37 | const result = await postcss([ 38 | cssnano, 39 | ]).process(opts.code, {from: opts.filename}); 40 | console.timeEnd('cssnano'); 41 | console.log(result.css.length + ' bytes'); 42 | console.log(''); 43 | } 44 | 45 | run(); 46 | -------------------------------------------------------------------------------- /c/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Devon Govett "] 3 | name = "lightningcss_c_bindings" 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | lightningcss = { path = "../", features = ["browserslist"] } 13 | parcel_sourcemap = { version = "2.1.1", features = ["json"] } 14 | browserslist-rs = { version = "0.17.0" } 15 | 16 | [build-dependencies] 17 | cbindgen = "0.24.3" 18 | -------------------------------------------------------------------------------- /c/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 5 | 6 | cbindgen::generate(crate_dir) 7 | .expect("Unable to generate bindings") 8 | .write_to_file("lightningcss.h"); 9 | } 10 | -------------------------------------------------------------------------------- /c/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | 3 | [parse] 4 | parse_deps = false 5 | include = ["lightningcss"] 6 | 7 | [export.rename] 8 | StyleSheetWrapper = "StyleSheet" 9 | 10 | [enum] 11 | prefix_with_name = true 12 | -------------------------------------------------------------------------------- /c/lightningcss.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | typedef struct CssError CssError; 7 | 8 | typedef struct StyleSheet StyleSheet; 9 | 10 | typedef struct Targets { 11 | uint32_t android; 12 | uint32_t chrome; 13 | uint32_t edge; 14 | uint32_t firefox; 15 | uint32_t ie; 16 | uint32_t ios_saf; 17 | uint32_t opera; 18 | uint32_t safari; 19 | uint32_t samsung; 20 | } Targets; 21 | 22 | typedef struct ParseOptions { 23 | const char *filename; 24 | bool nesting; 25 | bool custom_media; 26 | bool css_modules; 27 | const char *css_modules_pattern; 28 | bool css_modules_dashed_idents; 29 | bool error_recovery; 30 | } ParseOptions; 31 | 32 | typedef struct TransformOptions { 33 | struct Targets targets; 34 | char **unused_symbols; 35 | uintptr_t unused_symbols_len; 36 | } TransformOptions; 37 | 38 | typedef struct RawString { 39 | char *text; 40 | uintptr_t len; 41 | } RawString; 42 | 43 | typedef enum CssModuleReference_Tag { 44 | /** 45 | * A local reference. 46 | */ 47 | CssModuleReference_Local, 48 | /** 49 | * A global reference. 50 | */ 51 | CssModuleReference_Global, 52 | /** 53 | * A reference to an export in a different file. 54 | */ 55 | CssModuleReference_Dependency, 56 | } CssModuleReference_Tag; 57 | 58 | typedef struct CssModuleReference_Local_Body { 59 | /** 60 | * The local (compiled) name for the reference. 61 | */ 62 | struct RawString name; 63 | } CssModuleReference_Local_Body; 64 | 65 | typedef struct CssModuleReference_Global_Body { 66 | /** 67 | * The referenced global name. 68 | */ 69 | struct RawString name; 70 | } CssModuleReference_Global_Body; 71 | 72 | typedef struct CssModuleReference_Dependency_Body { 73 | /** 74 | * The name to reference within the dependency. 75 | */ 76 | struct RawString name; 77 | /** 78 | * The dependency specifier for the referenced file. 79 | */ 80 | struct RawString specifier; 81 | } CssModuleReference_Dependency_Body; 82 | 83 | typedef struct CssModuleReference { 84 | CssModuleReference_Tag tag; 85 | union { 86 | CssModuleReference_Local_Body local; 87 | CssModuleReference_Global_Body global; 88 | CssModuleReference_Dependency_Body dependency; 89 | }; 90 | } CssModuleReference; 91 | 92 | typedef struct CssModuleExport { 93 | struct RawString exported; 94 | struct RawString local; 95 | bool is_referenced; 96 | struct CssModuleReference *composes; 97 | uintptr_t composes_len; 98 | } CssModuleExport; 99 | 100 | typedef struct CssModulePlaceholder { 101 | struct RawString placeholder; 102 | struct CssModuleReference reference; 103 | } CssModulePlaceholder; 104 | 105 | typedef struct ToCssResult { 106 | struct RawString code; 107 | struct RawString map; 108 | struct CssModuleExport *exports; 109 | uintptr_t exports_len; 110 | struct CssModulePlaceholder *references; 111 | uintptr_t references_len; 112 | } ToCssResult; 113 | 114 | typedef struct PseudoClasses { 115 | const char *hover; 116 | const char *active; 117 | const char *focus; 118 | const char *focus_visible; 119 | const char *focus_within; 120 | } PseudoClasses; 121 | 122 | typedef struct ToCssOptions { 123 | bool minify; 124 | bool source_map; 125 | const char *input_source_map; 126 | uintptr_t input_source_map_len; 127 | const char *project_root; 128 | struct Targets targets; 129 | bool analyze_dependencies; 130 | struct PseudoClasses pseudo_classes; 131 | } ToCssOptions; 132 | 133 | bool lightningcss_browserslist_to_targets(const char *query, 134 | struct Targets *targets, 135 | struct CssError **error); 136 | 137 | struct StyleSheet *lightningcss_stylesheet_parse(const char *source, 138 | uintptr_t len, 139 | struct ParseOptions options, 140 | struct CssError **error); 141 | 142 | bool lightningcss_stylesheet_transform(struct StyleSheet *stylesheet, 143 | struct TransformOptions options, 144 | struct CssError **error); 145 | 146 | struct ToCssResult lightningcss_stylesheet_to_css(struct StyleSheet *stylesheet, 147 | struct ToCssOptions options, 148 | struct CssError **error); 149 | 150 | void lightningcss_stylesheet_free(struct StyleSheet *stylesheet); 151 | 152 | void lightningcss_to_css_result_free(struct ToCssResult result); 153 | 154 | const char *lightningcss_error_message(struct CssError *error); 155 | 156 | void lightningcss_error_free(struct CssError *error); 157 | 158 | uintptr_t lightningcss_stylesheet_get_warning_count(struct StyleSheet *stylesheet); 159 | 160 | const char *lightningcss_stylesheet_get_warning(struct StyleSheet *stylesheet, uintptr_t index); 161 | -------------------------------------------------------------------------------- /c/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "lightningcss.h" 4 | 5 | int print_error(CssError *error); 6 | 7 | int main() 8 | { 9 | char *source = 10 | ".foo {" 11 | " color: lch(50.998% 135.363 338);" 12 | "}" 13 | ".bar {" 14 | " color: yellow;" 15 | " composes: foo from './bar.css';" 16 | "}" 17 | ".baz:hover {" 18 | " color: var(--foo from './baz.css');" 19 | "}"; 20 | 21 | ParseOptions parse_opts = { 22 | .filename = "test.css", 23 | .css_modules = true, 24 | .css_modules_pattern = "yo_[name]_[local]", 25 | .css_modules_dashed_idents = true}; 26 | 27 | CssError *error = NULL; 28 | StyleSheet *stylesheet = lightningcss_stylesheet_parse(source, strlen(source), parse_opts, &error); 29 | if (!stylesheet) 30 | goto cleanup; 31 | 32 | char *unused_symbols[1] = {"bar"}; 33 | TransformOptions transform_opts = { 34 | .unused_symbols = unused_symbols, 35 | .unused_symbols_len = 0}; 36 | 37 | if (!lightningcss_browserslist_to_targets("last 2 versions, not IE <= 11", &transform_opts.targets, &error)) 38 | goto cleanup; 39 | 40 | if (!lightningcss_stylesheet_transform(stylesheet, transform_opts, &error)) 41 | goto cleanup; 42 | 43 | ToCssOptions to_css_opts = { 44 | .minify = true, 45 | .source_map = true, 46 | .pseudo_classes = { 47 | .hover = "is-hovered"}}; 48 | 49 | ToCssResult result = lightningcss_stylesheet_to_css(stylesheet, to_css_opts, &error); 50 | if (error) 51 | goto cleanup; 52 | 53 | size_t warning_count = lightningcss_stylesheet_get_warning_count(stylesheet); 54 | for (size_t i = 0; i < warning_count; i++) 55 | { 56 | printf("warning: %s\n", lightningcss_stylesheet_get_warning(stylesheet, i)); 57 | } 58 | 59 | fwrite(result.code.text, sizeof(char), result.code.len, stdout); 60 | printf("\n"); 61 | fwrite(result.map.text, sizeof(char), result.map.len, stdout); 62 | printf("\n"); 63 | 64 | for (int i = 0; i < result.exports_len; i++) 65 | { 66 | printf("%.*s -> %.*s\n", (int)result.exports[i].exported.len, result.exports[i].exported.text, (int)result.exports[i].local.len, result.exports[i].local.text); 67 | for (int j = 0; j < result.exports[i].composes_len; j++) 68 | { 69 | const CssModuleReference *ref = &result.exports[i].composes[j]; 70 | switch (ref->tag) 71 | { 72 | case CssModuleReference_Local: 73 | printf(" composes local: %.*s\n", (int)ref->local.name.len, ref->local.name.text); 74 | break; 75 | case CssModuleReference_Global: 76 | printf(" composes global: %.*s\n", (int)ref->global.name.len, ref->global.name.text); 77 | break; 78 | case CssModuleReference_Dependency: 79 | printf(" composes dependency: %.*s from %.*s\n", (int)ref->dependency.name.len, ref->dependency.name.text, (int)ref->dependency.specifier.len, ref->dependency.specifier.text); 80 | break; 81 | } 82 | } 83 | } 84 | 85 | for (int i = 0; i < result.references_len; i++) 86 | { 87 | printf("placeholder: %.*s\n", (int)result.references[i].placeholder.len, result.references[i].placeholder.text); 88 | } 89 | 90 | cleanup: 91 | lightningcss_stylesheet_free(stylesheet); 92 | lightningcss_to_css_result_free(result); 93 | 94 | if (error) 95 | { 96 | printf("error: %s\n", lightningcss_error_message(error)); 97 | lightningcss_error_free(error); 98 | return 1; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | package.json 2 | README.md 3 | .DS_Store 4 | lightningcss.exe 5 | -------------------------------------------------------------------------------- /cli/lightningcss: -------------------------------------------------------------------------------- 1 | This file is required so that npm creates the lightningcss binary on Windows. 2 | -------------------------------------------------------------------------------- /cli/postinstall.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let path = require('path'); 3 | 4 | let parts = [process.platform, process.arch]; 5 | if (process.platform === 'linux') { 6 | const {MUSL, familySync} = require('detect-libc'); 7 | const family = familySync(); 8 | if (family === MUSL) { 9 | parts.push('musl'); 10 | } else if (process.arch === 'arm') { 11 | parts.push('gnueabihf'); 12 | } else { 13 | parts.push('gnu'); 14 | } 15 | } else if (process.platform === 'win32') { 16 | parts.push('msvc'); 17 | } 18 | 19 | let binary = process.platform === 'win32' ? 'lightningcss.exe' : 'lightningcss'; 20 | 21 | let pkgPath; 22 | try { 23 | pkgPath = path.dirname(require.resolve(`lightningcss-cli-${parts.join('-')}/package.json`)); 24 | } catch (err) { 25 | pkgPath = path.join(__dirname, '..', 'target', 'release'); 26 | if (!fs.existsSync(path.join(pkgPath, binary))) { 27 | pkgPath = path.join(__dirname, '..', 'target', 'debug'); 28 | } 29 | } 30 | 31 | try { 32 | fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, binary)); 33 | } catch (err) { 34 | try { 35 | fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, binary)); 36 | } catch (err) { 37 | console.error('Failed to move lightningcss-cli binary into place.'); 38 | process.exit(1); 39 | } 40 | } 41 | 42 | if (process.platform === 'win32') { 43 | try { 44 | fs.unlinkSync(path.join(__dirname, 'lightningcss')); 45 | } catch (err) { } 46 | } 47 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Devon Govett "] 3 | name = "lightningcss-derive" 4 | description = "Derive macros for lightningcss" 5 | version = "1.0.0-alpha.43" 6 | license = "MPL-2.0" 7 | edition = "2021" 8 | repository = "https://github.com/parcel-bundler/lightningcss" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "1.0", features = ["extra-traits"] } 15 | quote = "1.0" 16 | proc-macro2 = "1.0" 17 | convert_case = "0.6.0" 18 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod parse; 4 | mod to_css; 5 | mod visit; 6 | 7 | #[proc_macro_derive(Visit, attributes(visit, skip_visit, skip_type, visit_types))] 8 | pub fn derive_visit_children(input: TokenStream) -> TokenStream { 9 | visit::derive_visit_children(input) 10 | } 11 | 12 | #[proc_macro_derive(Parse, attributes(css))] 13 | pub fn derive_parse(input: TokenStream) -> TokenStream { 14 | parse::derive_parse(input) 15 | } 16 | 17 | #[proc_macro_derive(ToCss, attributes(css))] 18 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 19 | to_css::derive_to_css(input) 20 | } 21 | -------------------------------------------------------------------------------- /derive/src/to_css.rs: -------------------------------------------------------------------------------- 1 | use convert_case::Casing; 2 | use proc_macro::{self, TokenStream}; 3 | use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; 4 | use quote::quote; 5 | use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type}; 6 | 7 | use crate::parse::CssOptions; 8 | 9 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 10 | let DeriveInput { 11 | ident, 12 | data, 13 | generics, 14 | attrs, 15 | .. 16 | } = parse_macro_input!(input); 17 | 18 | let opts = CssOptions::parse_attributes(&attrs).unwrap(); 19 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 20 | 21 | let imp = match &data { 22 | Data::Enum(data) => derive_enum(&data, &opts), 23 | _ => todo!(), 24 | }; 25 | 26 | let output = quote! { 27 | impl #impl_generics ToCss for #ident #ty_generics #where_clause { 28 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 29 | where 30 | W: std::fmt::Write, 31 | { 32 | #imp 33 | } 34 | } 35 | }; 36 | 37 | output.into() 38 | } 39 | 40 | fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 { 41 | let variants = data 42 | .variants 43 | .iter() 44 | .map(|variant| { 45 | let name = &variant.ident; 46 | let fields = variant 47 | .fields 48 | .iter() 49 | .enumerate() 50 | .map(|(index, field)| { 51 | field.ident.as_ref().map_or_else( 52 | || Ident::new(&format!("_{}", index), Span::call_site()), 53 | |ident| ident.clone(), 54 | ) 55 | }) 56 | .collect::>(); 57 | 58 | #[derive(PartialEq)] 59 | enum NeedsSpace { 60 | Yes, 61 | No, 62 | Maybe, 63 | } 64 | 65 | let mut needs_space = NeedsSpace::No; 66 | let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable(); 67 | let mut writes = Vec::new(); 68 | let mut has_needs_space = false; 69 | while let Some((field, name)) = fields_iter.next() { 70 | writes.push(if fields.len() > 1 { 71 | let space = match needs_space { 72 | NeedsSpace::Yes => quote! { dest.write_char(' ')?; }, 73 | NeedsSpace::No => quote! {}, 74 | NeedsSpace::Maybe => { 75 | has_needs_space = true; 76 | quote! { 77 | if needs_space { 78 | dest.write_char(' ')?; 79 | } 80 | } 81 | } 82 | }; 83 | 84 | if is_option(&field.ty) { 85 | needs_space = NeedsSpace::Maybe; 86 | let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) { 87 | // If the next field is non-optional, just insert the space here. 88 | needs_space = NeedsSpace::No; 89 | quote! { dest.write_char(' ')?; } 90 | } else { 91 | quote! {} 92 | }; 93 | quote! { 94 | if let Some(v) = #name { 95 | #space 96 | v.to_css(dest)?; 97 | #after_space 98 | } 99 | } 100 | } else { 101 | needs_space = NeedsSpace::Yes; 102 | quote! { 103 | #space 104 | #name.to_css(dest)?; 105 | } 106 | } 107 | } else { 108 | quote! { #name.to_css(dest) } 109 | }); 110 | } 111 | 112 | if writes.len() > 1 { 113 | writes.push(quote! { Ok(()) }); 114 | } 115 | 116 | if has_needs_space { 117 | writes.insert(0, quote! { let mut needs_space = false }); 118 | } 119 | 120 | match variant.fields { 121 | Fields::Unit => { 122 | let s = Literal::string(&variant.ident.to_string().to_case(opts.case)); 123 | quote! { 124 | Self::#name => dest.write_str(#s) 125 | } 126 | } 127 | Fields::Named(_) => { 128 | quote! { 129 | Self::#name { #(#fields),* } => { 130 | #(#writes)* 131 | } 132 | } 133 | } 134 | Fields::Unnamed(_) => { 135 | quote! { 136 | Self::#name(#(#fields),*) => { 137 | #(#writes)* 138 | } 139 | } 140 | } 141 | } 142 | }) 143 | .collect::>(); 144 | 145 | let output = quote! { 146 | match self { 147 | #(#variants),* 148 | } 149 | }; 150 | 151 | output.into() 152 | } 153 | 154 | fn is_option(ty: &Type) -> bool { 155 | matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option") 156 | } 157 | -------------------------------------------------------------------------------- /examples/schema.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "jsonschema")] 3 | { 4 | let schema = schemars::schema_for!(lightningcss::stylesheet::StyleSheet); 5 | let output = serde_json::to_string_pretty(&schema).unwrap(); 6 | let _ = std::fs::write("node/ast.json", output); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/serialize.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | parse(); 3 | } 4 | 5 | #[cfg(feature = "serde")] 6 | fn parse() { 7 | use lightningcss::stylesheet::{ParserOptions, StyleSheet}; 8 | use std::{env, fs}; 9 | 10 | let args: Vec = env::args().collect(); 11 | let contents = fs::read_to_string(&args[1]).unwrap(); 12 | let stylesheet = StyleSheet::parse( 13 | &contents, 14 | ParserOptions { 15 | filename: args[1].clone(), 16 | ..ParserOptions::default() 17 | }, 18 | ) 19 | .unwrap(); 20 | let json = serde_json::to_string(&stylesheet).unwrap(); 21 | println!("{}", json); 22 | } 23 | 24 | #[cfg(not(feature = "serde"))] 25 | fn parse() { 26 | panic!("serde feature is not enabled") 27 | } 28 | -------------------------------------------------------------------------------- /napi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Devon Govett "] 3 | name = "lightningcss-napi" 4 | version = "0.4.4" 5 | description = "Node-API bindings for Lightning CSS" 6 | license = "MPL-2.0" 7 | repository = "https://github.com/parcel-bundler/lightningcss" 8 | edition = "2021" 9 | 10 | [features] 11 | default = [] 12 | visitor = ["lightningcss/visitor"] 13 | bundler = ["dep:crossbeam-channel", "dep:rayon"] 14 | 15 | [dependencies] 16 | serde = { version = "1.0.201", features = ["derive"] } 17 | serde_bytes = "0.11.5" 18 | cssparser = "0.33.0" 19 | lightningcss = { version = "1.0.0-alpha.66", path = "../", features = [ 20 | "nodejs", 21 | "serde", 22 | ] } 23 | parcel_sourcemap = { version = "2.1.1", features = ["json"] } 24 | serde-detach = "0.0.1" 25 | smallvec = { version = "1.7.0", features = ["union"] } 26 | napi = { version = "2", default-features = false, features = [ 27 | "napi4", 28 | "napi5", 29 | "serde-json", 30 | ] } 31 | crossbeam-channel = { version = "0.5.6", optional = true } 32 | rayon = { version = "1.5.1", optional = true } 33 | -------------------------------------------------------------------------------- /napi/src/utils.rs: -------------------------------------------------------------------------------- 1 | use napi::{Error, JsObject, JsUnknown, Result}; 2 | 3 | // Workaround for https://github.com/napi-rs/napi-rs/issues/1641 4 | pub fn get_named_property>(obj: &JsObject, property: &str) -> Result { 5 | let unknown = obj.get_named_property::(property)?; 6 | T::try_from(unknown) 7 | } 8 | -------------------------------------------------------------------------------- /node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Devon Govett "] 3 | name = "lightningcss_node" 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | lightningcss-napi = { version = "0.4.4", path = "../napi", features = [ 13 | "bundler", 14 | "visitor", 15 | ] } 16 | napi = { version = "2.15.4", default-features = false, features = [ 17 | "compat-mode", 18 | ] } 19 | napi-derive = "2" 20 | 21 | [target.'cfg(target_os = "macos")'.dependencies] 22 | jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } 23 | 24 | [target.'cfg(not(target_arch = "wasm32"))'.build-dependencies] 25 | napi-build = "1" 26 | -------------------------------------------------------------------------------- /node/browserslistToTargets.js: -------------------------------------------------------------------------------- 1 | const BROWSER_MAPPING = { 2 | and_chr: 'chrome', 3 | and_ff: 'firefox', 4 | ie_mob: 'ie', 5 | op_mob: 'opera', 6 | and_qq: null, 7 | and_uc: null, 8 | baidu: null, 9 | bb: null, 10 | kaios: null, 11 | op_mini: null, 12 | }; 13 | 14 | function browserslistToTargets(browserslist) { 15 | let targets = {}; 16 | for (let browser of browserslist) { 17 | let [name, v] = browser.split(' '); 18 | if (BROWSER_MAPPING[name] === null) { 19 | continue; 20 | } 21 | 22 | let version = parseVersion(v); 23 | if (version == null) { 24 | continue; 25 | } 26 | 27 | if (targets[name] == null || version < targets[name]) { 28 | targets[name] = version; 29 | } 30 | } 31 | 32 | return targets; 33 | } 34 | 35 | function parseVersion(version) { 36 | let [major, minor = 0, patch = 0] = version 37 | .split('-')[0] 38 | .split('.') 39 | .map(v => parseInt(v, 10)); 40 | 41 | if (isNaN(major) || isNaN(minor) || isNaN(patch)) { 42 | return null; 43 | } 44 | 45 | return (major << 16) | (minor << 8) | patch; 46 | } 47 | 48 | module.exports = browserslistToTargets; 49 | -------------------------------------------------------------------------------- /node/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | extern crate napi_build; 3 | 4 | fn main() { 5 | #[cfg(not(target_arch = "wasm32"))] 6 | napi_build::setup(); 7 | } 8 | -------------------------------------------------------------------------------- /node/flags.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated by build-prefixes.js. DO NOT EDIT! 2 | 3 | exports.Features = { 4 | Nesting: 1, 5 | NotSelectorList: 2, 6 | DirSelector: 4, 7 | LangSelectorList: 8, 8 | IsSelector: 16, 9 | TextDecorationThicknessPercent: 32, 10 | MediaIntervalSyntax: 64, 11 | MediaRangeSyntax: 128, 12 | CustomMediaQueries: 256, 13 | ClampFunction: 512, 14 | ColorFunction: 1024, 15 | OklabColors: 2048, 16 | LabColors: 4096, 17 | P3Colors: 8192, 18 | HexAlphaColors: 16384, 19 | SpaceSeparatedColorNotation: 32768, 20 | FontFamilySystemUi: 65536, 21 | DoublePositionGradients: 131072, 22 | VendorPrefixes: 262144, 23 | LogicalProperties: 524288, 24 | LightDark: 1048576, 25 | Selectors: 31, 26 | MediaQueries: 448, 27 | Colors: 1113088, 28 | }; 29 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | let parts = [process.platform, process.arch]; 2 | if (process.platform === 'linux') { 3 | const { MUSL, familySync } = require('detect-libc'); 4 | const family = familySync(); 5 | if (family === MUSL) { 6 | parts.push('musl'); 7 | } else if (process.arch === 'arm') { 8 | parts.push('gnueabihf'); 9 | } else { 10 | parts.push('gnu'); 11 | } 12 | } else if (process.platform === 'win32') { 13 | parts.push('msvc'); 14 | } 15 | 16 | if (process.env.CSS_TRANSFORMER_WASM) { 17 | module.exports = require(`../pkg`); 18 | } else { 19 | try { 20 | module.exports = require(`lightningcss-${parts.join('-')}`); 21 | } catch (err) { 22 | module.exports = require(`../lightningcss.${parts.join('-')}.node`); 23 | } 24 | } 25 | 26 | module.exports.browserslistToTargets = require('./browserslistToTargets'); 27 | module.exports.composeVisitors = require('./composeVisitors'); 28 | module.exports.Features = require('./flags').Features; 29 | -------------------------------------------------------------------------------- /node/index.mjs: -------------------------------------------------------------------------------- 1 | import index from './index.js'; 2 | 3 | const { transform, transformStyleAttribute, bundle, bundleAsync, browserslistToTargets, composeVisitors, Features } = index; 4 | export { transform, transformStyleAttribute, bundle, bundleAsync, browserslistToTargets, composeVisitors, Features }; 5 | -------------------------------------------------------------------------------- /node/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | #[global_allocator] 3 | static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; 4 | 5 | use napi::{CallContext, JsObject, JsUnknown}; 6 | use napi_derive::{js_function, module_exports}; 7 | 8 | #[js_function(1)] 9 | fn transform(ctx: CallContext) -> napi::Result { 10 | lightningcss_napi::transform(ctx) 11 | } 12 | 13 | #[js_function(1)] 14 | fn transform_style_attribute(ctx: CallContext) -> napi::Result { 15 | lightningcss_napi::transform_style_attribute(ctx) 16 | } 17 | 18 | #[js_function(1)] 19 | pub fn bundle(ctx: CallContext) -> napi::Result { 20 | lightningcss_napi::bundle(ctx) 21 | } 22 | 23 | #[cfg(not(target_arch = "wasm32"))] 24 | #[js_function(1)] 25 | pub fn bundle_async(ctx: CallContext) -> napi::Result { 26 | lightningcss_napi::bundle_async(ctx) 27 | } 28 | 29 | #[cfg_attr(not(target_arch = "wasm32"), module_exports)] 30 | fn init(mut exports: JsObject) -> napi::Result<()> { 31 | exports.create_named_method("transform", transform)?; 32 | exports.create_named_method("transformStyleAttribute", transform_style_attribute)?; 33 | exports.create_named_method("bundle", bundle)?; 34 | #[cfg(not(target_arch = "wasm32"))] 35 | { 36 | exports.create_named_method("bundleAsync", bundle_async)?; 37 | } 38 | 39 | Ok(()) 40 | } 41 | 42 | #[cfg(target_arch = "wasm32")] 43 | #[no_mangle] 44 | pub fn register_module() { 45 | unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> { 46 | use napi::{Env, JsObject, NapiValue}; 47 | 48 | let env = Env::from_raw(raw_env); 49 | let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); 50 | init(exports) 51 | } 52 | 53 | napi::bindgen_prelude::register_module_exports(register) 54 | } 55 | 56 | #[cfg(target_arch = "wasm32")] 57 | #[no_mangle] 58 | pub extern "C" fn napi_wasm_malloc(size: usize) -> *mut u8 { 59 | use std::alloc::{alloc, Layout}; 60 | use std::mem; 61 | 62 | let align = mem::align_of::(); 63 | if let Ok(layout) = Layout::from_size_align(size, align) { 64 | unsafe { 65 | if layout.size() > 0 { 66 | let ptr = alloc(layout); 67 | if !ptr.is_null() { 68 | return ptr; 69 | } 70 | } else { 71 | return align as *mut u8; 72 | } 73 | } 74 | } 75 | 76 | std::process::abort(); 77 | } 78 | -------------------------------------------------------------------------------- /node/targets.d.ts: -------------------------------------------------------------------------------- 1 | // This file is autogenerated by build-prefixes.js. DO NOT EDIT! 2 | 3 | export interface Targets { 4 | android?: number, 5 | chrome?: number, 6 | edge?: number, 7 | firefox?: number, 8 | ie?: number, 9 | ios_saf?: number, 10 | opera?: number, 11 | safari?: number, 12 | samsung?: number 13 | } 14 | 15 | export const Features: { 16 | Nesting: 1, 17 | NotSelectorList: 2, 18 | DirSelector: 4, 19 | LangSelectorList: 8, 20 | IsSelector: 16, 21 | TextDecorationThicknessPercent: 32, 22 | MediaIntervalSyntax: 64, 23 | MediaRangeSyntax: 128, 24 | CustomMediaQueries: 256, 25 | ClampFunction: 512, 26 | ColorFunction: 1024, 27 | OklabColors: 2048, 28 | LabColors: 4096, 29 | P3Colors: 8192, 30 | HexAlphaColors: 16384, 31 | SpaceSeparatedColorNotation: 32768, 32 | FontFamilySystemUi: 65536, 33 | DoublePositionGradients: 131072, 34 | VendorPrefixes: 262144, 35 | LogicalProperties: 524288, 36 | LightDark: 1048576, 37 | Selectors: 31, 38 | MediaQueries: 448, 39 | Colors: 1113088, 40 | }; 41 | -------------------------------------------------------------------------------- /node/test/transform.test.mjs: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import {webcrypto as crypto} from 'node:crypto'; 4 | 5 | let transform, Features; 6 | if (process.env.TEST_WASM === 'node') { 7 | ({transform, Features} = await import('../../wasm/wasm-node.mjs')); 8 | } else if (process.env.TEST_WASM === 'browser') { 9 | // Define crypto globally for old node. 10 | // @ts-ignore 11 | globalThis.crypto ??= crypto; 12 | let wasm = await import('../../wasm/index.mjs'); 13 | await wasm.default(); 14 | ({transform, Features} = wasm); 15 | } else { 16 | ({transform, Features} = await import('../index.mjs')); 17 | } 18 | 19 | test('can enable non-standard syntax', () => { 20 | let res = transform({ 21 | filename: 'test.css', 22 | code: Buffer.from('.foo >>> .bar { color: red }'), 23 | nonStandard: { 24 | deepSelectorCombinator: true 25 | }, 26 | minify: true 27 | }); 28 | 29 | assert.equal(res.code.toString(), '.foo>>>.bar{color:red}'); 30 | }); 31 | 32 | test('can enable features without targets', () => { 33 | let res = transform({ 34 | filename: 'test.css', 35 | code: Buffer.from('.foo { .bar { color: red }}'), 36 | minify: true, 37 | include: Features.Nesting 38 | }); 39 | 40 | assert.equal(res.code.toString(), '.foo .bar{color:red}'); 41 | }); 42 | 43 | test('can disable features', () => { 44 | let res = transform({ 45 | filename: 'test.css', 46 | code: Buffer.from('.foo { color: lch(50.998% 135.363 338) }'), 47 | minify: true, 48 | targets: { 49 | chrome: 80 << 16 50 | }, 51 | exclude: Features.Colors 52 | }); 53 | 54 | assert.equal(res.code.toString(), '.foo{color:lch(50.998% 135.363 338)}'); 55 | }); 56 | 57 | test('can disable prefixing', () => { 58 | let res = transform({ 59 | filename: 'test.css', 60 | code: Buffer.from('.foo { user-select: none }'), 61 | minify: true, 62 | targets: { 63 | safari: 15 << 16 64 | }, 65 | exclude: Features.VendorPrefixes 66 | }); 67 | 68 | assert.equal(res.code.toString(), '.foo{user-select:none}'); 69 | }); 70 | 71 | test.run(); 72 | -------------------------------------------------------------------------------- /node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["*.d.ts"], 3 | "compilerOptions": { 4 | "lib": ["ES2020"], 5 | "moduleResolution": "node", 6 | "isolatedModules": true, 7 | "noEmit": true, 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightningcss", 3 | "version": "1.30.1", 4 | "license": "MPL-2.0", 5 | "description": "A CSS parser, transformer, and minifier written in Rust", 6 | "main": "node/index.js", 7 | "types": "node/index.d.ts", 8 | "exports": { 9 | "types": "./node/index.d.ts", 10 | "import": "./node/index.mjs", 11 | "require": "./node/index.js" 12 | }, 13 | "browserslist": "last 2 versions, not dead", 14 | "targets": { 15 | "main": false, 16 | "types": false 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "funding": { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/parcel" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/parcel-bundler/lightningcss.git" 28 | }, 29 | "engines": { 30 | "node": ">= 12.0.0" 31 | }, 32 | "napi": { 33 | "name": "lightningcss" 34 | }, 35 | "files": [ 36 | "node/*.js", 37 | "node/*.mjs", 38 | "node/*.d.ts", 39 | "node/*.flow" 40 | ], 41 | "dependencies": { 42 | "detect-libc": "^2.0.3" 43 | }, 44 | "devDependencies": { 45 | "@babel/parser": "7.21.4", 46 | "@babel/traverse": "7.21.4", 47 | "@codemirror/lang-css": "^6.0.1", 48 | "@codemirror/lang-javascript": "^6.1.2", 49 | "@codemirror/lint": "^6.1.0", 50 | "@codemirror/theme-one-dark": "^6.1.0", 51 | "@mdn/browser-compat-data": "~6.0.13", 52 | "@napi-rs/cli": "^2.14.0", 53 | "autoprefixer": "^10.4.21", 54 | "caniuse-lite": "^1.0.30001717", 55 | "codemirror": "^6.0.1", 56 | "cssnano": "^7.0.6", 57 | "esbuild": "^0.19.8", 58 | "flowgen": "^1.21.0", 59 | "jest-diff": "^27.4.2", 60 | "json-schema-to-typescript": "^11.0.2", 61 | "markdown-it-anchor": "^8.6.6", 62 | "markdown-it-prism": "^2.3.0", 63 | "markdown-it-table-of-contents": "^0.6.0", 64 | "napi-wasm": "^1.0.1", 65 | "node-fetch": "^3.1.0", 66 | "parcel": "^2.8.2", 67 | "patch-package": "^6.5.0", 68 | "path-browserify": "^1.0.1", 69 | "postcss": "^8.3.11", 70 | "posthtml-include": "^1.7.4", 71 | "posthtml-markdownit": "^1.3.1", 72 | "posthtml-prism": "^1.0.4", 73 | "process": "^0.11.10", 74 | "puppeteer": "^12.0.1", 75 | "recast": "^0.22.0", 76 | "sharp": "^0.33.5", 77 | "typescript": "^5.7.2", 78 | "util": "^0.12.4", 79 | "uvu": "^0.5.6" 80 | }, 81 | "resolutions": { 82 | "lightningcss": "link:." 83 | }, 84 | "scripts": { 85 | "prepare": "patch-package", 86 | "build": "node scripts/build.js && node scripts/build-flow.js", 87 | "build-release": "node scripts/build.js --release && node scripts/build-flow.js", 88 | "prepublishOnly": "node scripts/build-flow.js", 89 | "wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && wasm-opt target/wasm32-unknown-unknown/debug/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js", 90 | "wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && wasm-opt target/wasm32-unknown-unknown/release/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js", 91 | "website:start": "parcel 'website/*.html' website/playground/index.html", 92 | "website:build": "yarn wasm:build-release && parcel build 'website/*.html' website/playground/index.html", 93 | "build-ast": "cargo run --example schema --features jsonschema && node scripts/build-ast.js", 94 | "tsc": "tsc -p node/tsconfig.json", 95 | "test": "uvu node/test" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /patches/@babel+types+7.26.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js 2 | index 31feb1e..a64b83d 100644 3 | --- a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js 4 | +++ b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js 5 | @@ -66,6 +66,13 @@ const keys = { 6 | InterfaceDeclaration: ["id"], 7 | TypeAlias: ["id"], 8 | OpaqueType: ["id"], 9 | + TSDeclareFunction: ["id"], 10 | + TSEnumDeclaration: ["id"], 11 | + TSImportEqualsDeclaration: ["id"], 12 | + TSInterfaceDeclaration: ["id"], 13 | + TSModuleDeclaration: ["id"], 14 | + TSNamespaceExportDeclaration: ["id"], 15 | + TSTypeAliasDeclaration: ["id"], 16 | CatchClause: ["param"], 17 | LabeledStatement: ["label"], 18 | UnaryExpression: ["argument"], 19 | -------------------------------------------------------------------------------- /patches/json-schema-to-typescript+11.0.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/json-schema-to-typescript/dist/src/parser.js b/node_modules/json-schema-to-typescript/dist/src/parser.js 2 | index fa9d2e4..3f65449 100644 3 | --- a/node_modules/json-schema-to-typescript/dist/src/parser.js 4 | +++ b/node_modules/json-schema-to-typescript/dist/src/parser.js 5 | @@ -1,6 +1,6 @@ 6 | "use strict"; 7 | var __assign = (this && this.__assign) || function () { 8 | - __assign = Object.assign || function(t) { 9 | + __assign = Object.assign || function (t) { 10 | for (var s, i = 1, n = arguments.length; i < n; i++) { 11 | s = arguments[i]; 12 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 13 | @@ -90,14 +90,27 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) { 14 | }; 15 | case 'ANY': 16 | return __assign(__assign({}, (options.unknownAny ? AST_1.T_UNKNOWN : AST_1.T_ANY)), { comment: schema.description, keyName: keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames) }); 17 | - case 'ANY_OF': 18 | - return { 19 | + case 'ANY_OF': { 20 | + let union = { 21 | comment: schema.description, 22 | keyName: keyName, 23 | standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), 24 | params: schema.anyOf.map(function (_) { return parse(_, options, undefined, processed, usedNames); }), 25 | type: 'UNION' 26 | }; 27 | + 28 | + if (schema.properties) { 29 | + let common = newInterface(schema, options, processed, usedNames, keyName, keyNameFromDefinition); 30 | + return { 31 | + comment: schema.description, 32 | + keyName, 33 | + standaloneName: union.standaloneName, 34 | + params: [common, union], 35 | + type: 'INTERSECTION' 36 | + }; 37 | + } 38 | + return union; 39 | + } 40 | case 'BOOLEAN': 41 | return { 42 | comment: schema.description, 43 | @@ -118,10 +131,12 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) { 44 | comment: schema.description, 45 | keyName: keyName, 46 | standaloneName: standaloneName(schema, keyNameFromDefinition !== null && keyNameFromDefinition !== void 0 ? keyNameFromDefinition : keyName, usedNames), 47 | - params: schema.enum.map(function (_, n) { return ({ 48 | - ast: parse(_, options, undefined, processed, usedNames), 49 | - keyName: schema.tsEnumNames[n] 50 | - }); }), 51 | + params: schema.enum.map(function (_, n) { 52 | + return ({ 53 | + ast: parse(_, options, undefined, processed, usedNames), 54 | + keyName: schema.tsEnumNames[n] 55 | + }); 56 | + }), 57 | type: 'ENUM' 58 | }; 59 | case 'NAMED_SCHEMA': 60 | @@ -147,14 +162,24 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) { 61 | standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), 62 | type: 'OBJECT' 63 | }; 64 | - case 'ONE_OF': 65 | + case 'ONE_OF': { 66 | + let common = schema.properties ? parseSchema(schema, options, processed, usedNames, keyName) : null; 67 | + let commonKeys = common ? new Set(common.map(p => p.keyName)) : null; 68 | + 69 | return { 70 | comment: schema.description, 71 | keyName: keyName, 72 | standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), 73 | - params: schema.oneOf.map(function (_) { return parse(_, options, undefined, processed, usedNames); }), 74 | + params: schema.oneOf.map(function (_) { 75 | + let item = parse(_, options, undefined, processed, usedNames); 76 | + if (common && item.type === 'INTERFACE') { 77 | + item.params = common.concat(item.params.filter(p => !commonKeys.has(p.keyName))); 78 | + } 79 | + return item; 80 | + }), 81 | type: 'UNION' 82 | }; 83 | + } 84 | case 'REFERENCE': 85 | throw Error((0, util_1.format)('Refs should have been resolved by the resolver!', schema)); 86 | case 'STRING': 87 | @@ -277,13 +302,15 @@ function parseSuperTypes(schema, options, processed, usedNames) { 88 | * Helper to parse schema properties into params on the parent schema's type 89 | */ 90 | function parseSchema(schema, options, processed, usedNames, parentSchemaName) { 91 | - var asts = (0, lodash_1.map)(schema.properties, function (value, key) { return ({ 92 | - ast: parse(value, options, key, processed, usedNames), 93 | - isPatternProperty: false, 94 | - isRequired: (0, lodash_1.includes)(schema.required || [], key), 95 | - isUnreachableDefinition: false, 96 | - keyName: key 97 | - }); }); 98 | + var asts = (0, lodash_1.map)(schema.properties, function (value, key) { 99 | + return ({ 100 | + ast: parse(value, options, key, processed, usedNames), 101 | + isPatternProperty: false, 102 | + isRequired: (0, lodash_1.includes)(schema.required || [], key), 103 | + isUnreachableDefinition: false, 104 | + keyName: key 105 | + }); 106 | + }); 107 | var singlePatternProperty = false; 108 | if (schema.patternProperties) { 109 | // partially support patternProperties. in the case that 110 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | chain_width = 80 3 | max_width = 115 -------------------------------------------------------------------------------- /scripts/build-flow.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { compiler, beautify } = require('flowgen'); 3 | 4 | let dir = `${__dirname}/../`; 5 | let contents = fs.readFileSync(dir + '/node/index.d.ts', 'utf8').replace('`${PropertyStart}${string}`', 'string'); 6 | contents = contents.replace(/`.*`/g, 'string'); 7 | contents = contents.replace(/(string & \{\})/g, 'string'); 8 | let index = beautify(compiler.compileDefinitionString(contents, { inexact: false, interfaceRecords: true })); 9 | index = index.replace('{ code: any }', '{| code: any |}'); 10 | index = index.replace(/from "(.*?)";/g, 'from "$1.js.flow";'); 11 | // This Exclude type isn't right at all, but idk how to get it working for real... 12 | fs.writeFileSync(dir + '/node/index.js.flow', '// @flow\n\ntype Exclude = A;\n' + index) 13 | 14 | let ast = beautify(compiler.compileDefinitionFile(dir + '/node/ast.d.ts', { inexact: false })); 15 | fs.writeFileSync(dir + '/node/ast.js.flow', '// @flow\n\n' + ast) 16 | 17 | let targets = beautify(compiler.compileDefinitionFile(dir + '/node/targets.d.ts', { inexact: false })); 18 | fs.writeFileSync(dir + '/node/targets.js.flow', '// @flow\n\n' + targets) 19 | -------------------------------------------------------------------------------- /scripts/build-npm.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const pkg = require('../package.json'); 3 | 4 | const dir = `${__dirname}/..`; 5 | 6 | // Add `libc` fields only to platforms that have libc(Standard C library). 7 | const triples = [ 8 | { 9 | name: 'x86_64-apple-darwin', 10 | }, 11 | { 12 | name: 'x86_64-unknown-linux-gnu', 13 | libc: 'glibc', 14 | }, 15 | { 16 | name: 'x86_64-pc-windows-msvc', 17 | }, 18 | { 19 | name: 'aarch64-pc-windows-msvc' 20 | }, 21 | { 22 | name: 'aarch64-apple-darwin', 23 | }, 24 | { 25 | name: 'aarch64-unknown-linux-gnu', 26 | libc: 'glibc', 27 | }, 28 | { 29 | name: 'armv7-unknown-linux-gnueabihf', 30 | }, 31 | { 32 | name: 'aarch64-unknown-linux-musl', 33 | libc: 'musl', 34 | }, 35 | { 36 | name: 'x86_64-unknown-linux-musl', 37 | libc: 'musl', 38 | }, 39 | { 40 | name: 'x86_64-unknown-freebsd' 41 | } 42 | ]; 43 | const cpuToNodeArch = { 44 | x86_64: 'x64', 45 | aarch64: 'arm64', 46 | i686: 'ia32', 47 | armv7: 'arm', 48 | }; 49 | const sysToNodePlatform = { 50 | linux: 'linux', 51 | freebsd: 'freebsd', 52 | darwin: 'darwin', 53 | windows: 'win32', 54 | }; 55 | 56 | let optionalDependencies = {}; 57 | let cliOptionalDependencies = {}; 58 | 59 | try { 60 | fs.mkdirSync(dir + '/npm'); 61 | } catch (err) { } 62 | 63 | for (let triple of triples) { 64 | // Add the libc field to package.json to avoid downloading both 65 | // `gnu` and `musl` packages in Linux. 66 | const libc = triple.libc; 67 | let [cpu, , os, abi] = triple.name.split('-'); 68 | cpu = cpuToNodeArch[cpu] || cpu; 69 | os = sysToNodePlatform[os] || os; 70 | 71 | let t = `${os}-${cpu}`; 72 | if (abi) { 73 | t += '-' + abi; 74 | } 75 | 76 | buildNode(triple.name, cpu, os, libc, t); 77 | buildCLI(triple.name, cpu, os, libc, t); 78 | } 79 | 80 | pkg.optionalDependencies = optionalDependencies; 81 | fs.writeFileSync(`${dir}/package.json`, JSON.stringify(pkg, false, 2) + '\n'); 82 | 83 | let cliPkg = { ...pkg }; 84 | cliPkg.name += '-cli'; 85 | cliPkg.bin = { 86 | 'lightningcss': 'lightningcss' 87 | }; 88 | delete cliPkg.main; 89 | delete cliPkg.napi; 90 | delete cliPkg.exports; 91 | delete cliPkg.devDependencies; 92 | delete cliPkg.targets; 93 | delete cliPkg.types; 94 | cliPkg.files = ['lightningcss', 'postinstall.js']; 95 | cliPkg.optionalDependencies = cliOptionalDependencies; 96 | cliPkg.scripts = { 97 | postinstall: 'node postinstall.js' 98 | }; 99 | 100 | fs.writeFileSync(`${dir}/cli/package.json`, JSON.stringify(cliPkg, false, 2) + '\n'); 101 | fs.copyFileSync(`${dir}/README.md`, `${dir}/cli/README.md`); 102 | fs.copyFileSync(`${dir}/LICENSE`, `${dir}/cli/LICENSE`); 103 | 104 | function buildNode(triple, cpu, os, libc, t) { 105 | let name = `lightningcss.${t}.node`; 106 | 107 | let pkg2 = { ...pkg }; 108 | pkg2.name += '-' + t; 109 | pkg2.os = [os]; 110 | pkg2.cpu = [cpu]; 111 | if (libc) { 112 | pkg2.libc = [libc]; 113 | } 114 | pkg2.main = name; 115 | pkg2.files = [name]; 116 | delete pkg2.exports; 117 | delete pkg2.napi; 118 | delete pkg2.devDependencies; 119 | delete pkg2.dependencies; 120 | delete pkg2.optionalDependencies; 121 | delete pkg2.targets; 122 | delete pkg2.scripts; 123 | delete pkg2.types; 124 | 125 | optionalDependencies[pkg2.name] = pkg.version; 126 | 127 | try { 128 | fs.mkdirSync(dir + '/npm/node-' + t); 129 | } catch (err) { } 130 | fs.writeFileSync(`${dir}/npm/node-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n'); 131 | fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${name}`, `${dir}/npm/node-${t}/${name}`); 132 | fs.writeFileSync(`${dir}/npm/node-${t}/README.md`, `This is the ${triple} build of lightningcss. See https://github.com/parcel-bundler/lightningcss for details.`); 133 | fs.copyFileSync(`${dir}/LICENSE`, `${dir}/npm/node-${t}/LICENSE`); 134 | } 135 | 136 | function buildCLI(triple, cpu, os, libc, t) { 137 | let binary = os === 'win32' ? 'lightningcss.exe' : 'lightningcss'; 138 | let pkg2 = { ...pkg }; 139 | pkg2.name += '-cli-' + t; 140 | pkg2.os = [os]; 141 | pkg2.cpu = [cpu]; 142 | pkg2.files = [binary]; 143 | if (libc) { 144 | pkg2.libc = [libc]; 145 | } 146 | delete pkg2.main; 147 | delete pkg2.exports; 148 | delete pkg2.napi; 149 | delete pkg2.devDependencies; 150 | delete pkg2.dependencies; 151 | delete pkg2.optionalDependencies; 152 | delete pkg2.targets; 153 | delete pkg2.scripts; 154 | delete pkg2.types; 155 | 156 | cliOptionalDependencies[pkg2.name] = pkg.version; 157 | 158 | try { 159 | fs.mkdirSync(dir + '/npm/cli-' + t); 160 | } catch (err) { } 161 | fs.writeFileSync(`${dir}/npm/cli-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n'); 162 | fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${binary}`, `${dir}/npm/cli-${t}/${binary}`); 163 | fs.chmodSync(`${dir}/npm/cli-${t}/${binary}`, 0o755); // Ensure execute bit is set. 164 | fs.writeFileSync(`${dir}/npm/cli-${t}/README.md`, `This is the ${triple} build of lightningcss-cli. See https://github.com/parcel-bundler/lightningcss for details.`); 165 | fs.copyFileSync(`${dir}/LICENSE`, `${dir}/npm/cli-${t}/LICENSE`); 166 | } 167 | -------------------------------------------------------------------------------- /scripts/build-wasm.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const exec = require('child_process').execSync; 3 | const fs = require('fs'); 4 | const pkg = require('../package.json'); 5 | 6 | const dir = `${__dirname}/..`; 7 | 8 | let b = fs.readFileSync(`${dir}/node/browserslistToTargets.js`, 'utf8'); 9 | b = b.replace('module.exports = browserslistToTargets;', 'export {browserslistToTargets};'); 10 | fs.writeFileSync(`${dir}/wasm/browserslistToTargets.js`, b); 11 | 12 | let flags = fs.readFileSync(`${dir}/node/flags.js`, 'utf8'); 13 | flags = flags.replace('exports.Features =', 'export const Features ='); 14 | fs.writeFileSync(`${dir}/wasm/flags.js`, flags); 15 | 16 | let composeVisitors = fs.readFileSync(`${dir}/node/composeVisitors.js`, 'utf8'); 17 | composeVisitors = composeVisitors.replace('module.exports = composeVisitors', 'export { composeVisitors }'); 18 | fs.writeFileSync(`${dir}/wasm/composeVisitors.js`, composeVisitors); 19 | 20 | let dts = fs.readFileSync(`${dir}/node/index.d.ts`, 'utf8'); 21 | dts = dts.replace(/: Buffer/g, ': Uint8Array'); 22 | dts += ` 23 | /** Initializes the web assembly module. */ 24 | export default function init(input?: string | URL | Request): Promise; 25 | `; 26 | fs.writeFileSync(`${dir}/wasm/index.d.ts`, dts); 27 | fs.copyFileSync(`${dir}/node/targets.d.ts`, `${dir}/wasm/targets.d.ts`); 28 | fs.copyFileSync(`${dir}/node/ast.d.ts`, `${dir}/wasm/ast.d.ts`); 29 | fs.cpSync(`${dir}/node_modules/napi-wasm`, `${dir}/wasm/node_modules/napi-wasm`, {recursive: true}); 30 | 31 | let readme = fs.readFileSync(`${dir}/README.md`, 'utf8'); 32 | readme = readme.replace('# ⚡️ Lightning CSS', '# ⚡️ lightningcss-wasm'); 33 | fs.writeFileSync(`${dir}/wasm/README.md`, readme); 34 | 35 | const cjsBuild = { 36 | entryPoints: [ 37 | `${dir}/wasm/wasm-node.mjs`, 38 | `${dir}/wasm/index.mjs`, 39 | ], 40 | bundle: true, 41 | format: 'cjs', 42 | platform: 'node', 43 | packages: 'external', 44 | outdir: `${dir}/wasm`, 45 | outExtension: { '.js': '.cjs' }, 46 | inject: [`${dir}/wasm/import.meta.url-polyfill.js`], 47 | define: { 'import.meta.url': 'import_meta_url' }, 48 | }; 49 | esbuild.build(cjsBuild).catch(console.error); 50 | 51 | let wasmPkg = { ...pkg }; 52 | wasmPkg.name = 'lightningcss-wasm'; 53 | wasmPkg.type = 'module'; 54 | wasmPkg.main = 'index.mjs'; 55 | wasmPkg.module = 'index.mjs'; 56 | wasmPkg.exports = { 57 | '.': { 58 | types: './index.d.ts', 59 | node: { 60 | import: './wasm-node.mjs', 61 | require: './wasm-node.cjs' 62 | }, 63 | default: { 64 | import: './index.mjs', 65 | require: './index.cjs' 66 | } 67 | }, 68 | // Allow esbuild to import the wasm file 69 | // without copying it in the src directory. 70 | // Simplifies loading it in the browser when used in a library. 71 | './lightningcss_node.wasm': './lightningcss_node.wasm' 72 | }; 73 | wasmPkg.types = 'index.d.ts'; 74 | wasmPkg.sideEffects = false; 75 | wasmPkg.files = ['*.js', '*.cjs', '*.mjs', '*.d.ts', '*.flow', '*.wasm']; 76 | wasmPkg.dependencies = { 77 | 'napi-wasm': pkg.devDependencies['napi-wasm'] 78 | }; 79 | wasmPkg.bundledDependencies = ['napi-wasm']; // for stackblitz 80 | delete wasmPkg.napi; 81 | delete wasmPkg.devDependencies; 82 | delete wasmPkg.optionalDependencies; 83 | delete wasmPkg.targets; 84 | delete wasmPkg.scripts; 85 | fs.writeFileSync(`${dir}/wasm/package.json`, JSON.stringify(wasmPkg, false, 2) + '\n'); 86 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const { spawn, execSync } = require('child_process'); 2 | 3 | let release = process.argv.includes('--release'); 4 | build().catch((err) => { 5 | console.error(err); 6 | process.exit(1); 7 | }); 8 | 9 | async function build() { 10 | if (process.platform === 'darwin') { 11 | setupMacBuild(); 12 | } 13 | 14 | await new Promise((resolve, reject) => { 15 | let args = ['build', '--platform', '--cargo-cwd', 'node']; 16 | if (release) { 17 | args.push('--release'); 18 | } 19 | 20 | if (process.env.RUST_TARGET) { 21 | args.push('--target', process.env.RUST_TARGET); 22 | } 23 | 24 | let yarn = spawn('napi', args, { 25 | stdio: 'inherit', 26 | cwd: __dirname + '/../', 27 | shell: true, 28 | }); 29 | 30 | yarn.on('error', reject); 31 | yarn.on('close', resolve); 32 | }); 33 | } 34 | 35 | // This forces Clang/LLVM to be used as a C compiler instead of GCC. 36 | // This is necessary for cross-compilation for Apple Silicon in GitHub Actions. 37 | function setupMacBuild() { 38 | process.env.CC = execSync('xcrun -f clang', { encoding: 'utf8' }).trim(); 39 | process.env.CXX = execSync('xcrun -f clang++', { encoding: 'utf8' }).trim(); 40 | 41 | let sysRoot = execSync('xcrun --sdk macosx --show-sdk-path', { 42 | encoding: 'utf8', 43 | }).trim(); 44 | process.env.CFLAGS = `-isysroot ${sysRoot} -isystem ${sysRoot}`; 45 | process.env.MACOSX_DEPLOYMENT_TARGET = '10.9'; 46 | } 47 | -------------------------------------------------------------------------------- /selectors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parcel_selectors" 3 | version = "0.28.2" 4 | authors = ["The Servo Project Developers"] 5 | documentation = "https://docs.rs/parcel_selectors/" 6 | description = "CSS Selectors matching for Rust - forked for lightningcss" 7 | repository = "https://github.com/parcel-bundler/lightningcss" 8 | readme = "README.md" 9 | keywords = ["css", "selectors"] 10 | license = "MPL-2.0" 11 | build = "build.rs" 12 | edition = "2021" 13 | 14 | [lib] 15 | name = "parcel_selectors" 16 | path = "lib.rs" 17 | 18 | [features] 19 | bench = [] 20 | jsonschema = ["serde", "schemars"] 21 | into_owned = ["static-self"] 22 | smallvec = ["static-self/smallvec"] 23 | serde = ["dep:serde", "smallvec/serde"] 24 | 25 | [dependencies] 26 | bitflags = "2.2.1" 27 | cssparser = "0.33.0" 28 | rustc-hash = "2" 29 | log = "0.4" 30 | phf = "0.11.2" 31 | precomputed-hash = "0.1" 32 | smallvec = "1.0" 33 | serde = { version = "1.0.201", features = ["derive"], optional = true } 34 | schemars = { version = "0.8.19", features = ["smallvec"], optional = true } 35 | static-self = { version = "0.1.2", path = "../static-self", optional = true } 36 | 37 | [build-dependencies] 38 | phf_codegen = "0.11" 39 | -------------------------------------------------------------------------------- /selectors/README.md: -------------------------------------------------------------------------------- 1 | rust-selectors 2 | ============== 3 | 4 | This is a fork of the `selectors` crate, updated to use the latest version of `cssparser`. 5 | 6 | * [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)]( 7 | https://travis-ci.com/servo/rust-selectors) 8 | * [Documentation](https://docs.rs/selectors/) 9 | * [crates.io](https://crates.io/crates/selectors) 10 | 11 | CSS Selectors library for Rust. 12 | Includes parsing and serialization of selectors, 13 | as well as matching against a generic tree of elements. 14 | Pseudo-elements and most pseudo-classes are generic as well. 15 | 16 | **Warning:** breaking changes are made to this library fairly frequently 17 | (13 times in 2016, for example). 18 | However you can use this crate without updating it that often, 19 | old versions stay available on crates.io and Cargo will only automatically update 20 | to versions that are numbered as compatible. 21 | 22 | To see how to use this library with your own tree representation, 23 | see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs). 24 | (Note however that Kuchiki is not always up to date with the latest rust-selectors version, 25 | so that code may need to be tweaked.) 26 | If you don’t already have a tree data structure, 27 | consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself. 28 | -------------------------------------------------------------------------------- /selectors/build.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use std::env; 6 | use std::fs::File; 7 | use std::io::{BufWriter, Write}; 8 | use std::path::Path; 9 | 10 | fn main() { 11 | let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("ascii_case_insensitive_html_attributes.rs"); 12 | let mut file = BufWriter::new(File::create(&path).unwrap()); 13 | 14 | let mut set = phf_codegen::Set::new(); 15 | for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { 16 | set.entry(name); 17 | } 18 | write!( 19 | &mut file, 20 | "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}", 21 | set.build(), 22 | ) 23 | .unwrap(); 24 | } 25 | 26 | /// 27 | static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#" 28 | accept 29 | accept-charset 30 | align 31 | alink 32 | axis 33 | bgcolor 34 | charset 35 | checked 36 | clear 37 | codetype 38 | color 39 | compact 40 | declare 41 | defer 42 | dir 43 | direction 44 | disabled 45 | enctype 46 | face 47 | frame 48 | hreflang 49 | http-equiv 50 | lang 51 | language 52 | link 53 | media 54 | method 55 | multiple 56 | nohref 57 | noresize 58 | noshade 59 | nowrap 60 | readonly 61 | rel 62 | rev 63 | rules 64 | scope 65 | scrolling 66 | selected 67 | shape 68 | target 69 | text 70 | type 71 | valign 72 | valuetype 73 | vlink 74 | "#; 75 | -------------------------------------------------------------------------------- /selectors/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // Make |cargo bench| work. 6 | #![cfg_attr(feature = "bench", feature(test))] 7 | 8 | #[macro_use] 9 | extern crate bitflags; 10 | #[macro_use] 11 | extern crate cssparser; 12 | #[macro_use] 13 | extern crate log; 14 | 15 | pub mod attr; 16 | pub mod bloom; 17 | mod builder; 18 | pub mod context; 19 | pub mod matching; 20 | mod nth_index_cache; 21 | pub mod parser; 22 | pub mod sink; 23 | mod tree; 24 | pub mod visitor; 25 | 26 | #[cfg(all(feature = "serde"))] 27 | mod serialization; 28 | 29 | pub use crate::nth_index_cache::NthIndexCache; 30 | pub use crate::parser::{Parser, SelectorImpl, SelectorList}; 31 | pub use crate::tree::{Element, OpaqueElement}; 32 | -------------------------------------------------------------------------------- /selectors/nth_index_cache.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::tree::OpaqueElement; 6 | use rustc_hash::FxHashMap; 7 | 8 | /// A cache to speed up matching of nth-index-like selectors. 9 | /// 10 | /// See [1] for some discussion around the design tradeoffs. 11 | /// 12 | /// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3 13 | #[derive(Default)] 14 | pub struct NthIndexCache { 15 | nth: NthIndexCacheInner, 16 | nth_last: NthIndexCacheInner, 17 | nth_of_type: NthIndexCacheInner, 18 | nth_last_of_type: NthIndexCacheInner, 19 | } 20 | 21 | impl NthIndexCache { 22 | /// Gets the appropriate cache for the given parameters. 23 | pub fn get(&mut self, is_of_type: bool, is_from_end: bool) -> &mut NthIndexCacheInner { 24 | match (is_of_type, is_from_end) { 25 | (false, false) => &mut self.nth, 26 | (false, true) => &mut self.nth_last, 27 | (true, false) => &mut self.nth_of_type, 28 | (true, true) => &mut self.nth_last_of_type, 29 | } 30 | } 31 | } 32 | 33 | /// The concrete per-pseudo-class cache. 34 | #[derive(Default)] 35 | pub struct NthIndexCacheInner(FxHashMap); 36 | 37 | impl NthIndexCacheInner { 38 | /// Does a lookup for a given element in the cache. 39 | pub fn lookup(&mut self, el: OpaqueElement) -> Option { 40 | self.0.get(&el).copied() 41 | } 42 | 43 | /// Inserts an entry into the cache. 44 | pub fn insert(&mut self, element: OpaqueElement, index: i32) { 45 | self.0.insert(element, index); 46 | } 47 | 48 | /// Returns whether the cache is empty. 49 | pub fn is_empty(&self) -> bool { 50 | self.0.is_empty() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /selectors/sink.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! Small helpers to abstract over different containers. 6 | #![deny(missing_docs)] 7 | 8 | use smallvec::{Array, SmallVec}; 9 | 10 | /// A trait to abstract over a `push` method that may be implemented for 11 | /// different kind of types. 12 | /// 13 | /// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a 14 | /// type which `push` method does only tweak a byte when we only need to check 15 | /// for the presence of something. 16 | pub trait Push { 17 | /// Push a value into self. 18 | fn push(&mut self, value: T); 19 | } 20 | 21 | impl Push for Vec { 22 | fn push(&mut self, value: T) { 23 | Vec::push(self, value); 24 | } 25 | } 26 | 27 | impl Push for SmallVec { 28 | fn push(&mut self, value: A::Item) { 29 | SmallVec::push(self, value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /selectors/tree.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! Traits that nodes must implement. Breaks the otherwise-cyclic dependency 6 | //! between layout and style. 7 | 8 | use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; 9 | use crate::matching::{ElementSelectorFlags, MatchingContext}; 10 | use crate::parser::SelectorImpl; 11 | use std::fmt::Debug; 12 | use std::ptr::NonNull; 13 | 14 | /// Opaque representation of an Element, for identity comparisons. 15 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 16 | pub struct OpaqueElement(NonNull<()>); 17 | 18 | unsafe impl Send for OpaqueElement {} 19 | 20 | impl OpaqueElement { 21 | /// Creates a new OpaqueElement from an arbitrarily-typed pointer. 22 | pub fn new(ptr: &T) -> Self { 23 | unsafe { OpaqueElement(NonNull::new_unchecked(ptr as *const T as *const () as *mut ())) } 24 | } 25 | } 26 | 27 | pub trait Element<'i>: Sized + Clone + Debug { 28 | type Impl: SelectorImpl<'i>; 29 | 30 | /// Converts self into an opaque representation. 31 | fn opaque(&self) -> OpaqueElement; 32 | 33 | fn parent_element(&self) -> Option; 34 | 35 | /// Whether the parent node of this element is a shadow root. 36 | fn parent_node_is_shadow_root(&self) -> bool; 37 | 38 | /// The host of the containing shadow root, if any. 39 | fn containing_shadow_host(&self) -> Option; 40 | 41 | /// The parent of a given pseudo-element, after matching a pseudo-element 42 | /// selector. 43 | /// 44 | /// This is guaranteed to be called in a pseudo-element. 45 | fn pseudo_element_originating_element(&self) -> Option { 46 | debug_assert!(self.is_pseudo_element()); 47 | self.parent_element() 48 | } 49 | 50 | /// Whether we're matching on a pseudo-element. 51 | fn is_pseudo_element(&self) -> bool; 52 | 53 | /// Skips non-element nodes 54 | fn prev_sibling_element(&self) -> Option; 55 | 56 | /// Skips non-element nodes 57 | fn next_sibling_element(&self) -> Option; 58 | 59 | fn is_html_element_in_html_document(&self) -> bool; 60 | 61 | fn has_local_name(&self, local_name: &>::BorrowedLocalName) -> bool; 62 | 63 | /// Empty string for no namespace 64 | fn has_namespace(&self, ns: &>::BorrowedNamespaceUrl) -> bool; 65 | 66 | /// Whether this element and the `other` element have the same local name and namespace. 67 | fn is_same_type(&self, other: &Self) -> bool; 68 | 69 | fn attr_matches( 70 | &self, 71 | ns: &NamespaceConstraint<&>::NamespaceUrl>, 72 | local_name: &>::LocalName, 73 | operation: &AttrSelectorOperation<&>::AttrValue>, 74 | ) -> bool; 75 | 76 | fn match_non_ts_pseudo_class( 77 | &self, 78 | pc: &>::NonTSPseudoClass, 79 | context: &mut MatchingContext<'_, 'i, Self::Impl>, 80 | flags_setter: &mut F, 81 | ) -> bool 82 | where 83 | F: FnMut(&Self, ElementSelectorFlags); 84 | 85 | fn match_pseudo_element( 86 | &self, 87 | pe: &>::PseudoElement, 88 | context: &mut MatchingContext<'_, 'i, Self::Impl>, 89 | ) -> bool; 90 | 91 | /// Whether this element is a `link`. 92 | fn is_link(&self) -> bool; 93 | 94 | /// Returns whether the element is an HTML element. 95 | fn is_html_slot_element(&self) -> bool; 96 | 97 | /// Returns the assigned element this element is assigned to. 98 | /// 99 | /// Necessary for the `::slotted` pseudo-class. 100 | fn assigned_slot(&self) -> Option { 101 | None 102 | } 103 | 104 | fn has_id(&self, id: &>::Identifier, case_sensitivity: CaseSensitivity) -> bool; 105 | 106 | fn has_class( 107 | &self, 108 | name: &>::Identifier, 109 | case_sensitivity: CaseSensitivity, 110 | ) -> bool; 111 | 112 | /// Returns the mapping from the `exportparts` attribute in the reverse 113 | /// direction, that is, in an outer-tree -> inner-tree direction. 114 | fn imported_part( 115 | &self, 116 | name: &>::Identifier, 117 | ) -> Option<>::Identifier>; 118 | 119 | fn is_part(&self, name: &>::Identifier) -> bool; 120 | 121 | /// Returns whether this element matches `:empty`. 122 | /// 123 | /// That is, whether it does not contain any child element or any non-zero-length text node. 124 | /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo 125 | fn is_empty(&self) -> bool; 126 | 127 | /// Returns whether this element matches `:root`, 128 | /// i.e. whether it is the root element of a document. 129 | /// 130 | /// Note: this can be false even if `.parent_element()` is `None` 131 | /// if the parent node is a `DocumentFragment`. 132 | fn is_root(&self) -> bool; 133 | 134 | /// Returns whether this element should ignore matching nth child 135 | /// selector. 136 | fn ignores_nth_child_selectors(&self) -> bool { 137 | false 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /selectors/visitor.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | //! Visitor traits for selectors. 6 | 7 | #![deny(missing_docs)] 8 | 9 | use crate::attr::NamespaceConstraint; 10 | use crate::parser::{Combinator, Component, Selector, SelectorImpl}; 11 | 12 | /// A trait to visit selector properties. 13 | /// 14 | /// All the `visit_foo` methods return a boolean indicating whether the 15 | /// traversal should continue or not. 16 | pub trait SelectorVisitor<'i>: Sized { 17 | /// The selector implementation this visitor wants to visit. 18 | type Impl: SelectorImpl<'i>; 19 | 20 | /// Visit an attribute selector that may match (there are other selectors 21 | /// that may never match, like those containing whitespace or the empty 22 | /// string). 23 | fn visit_attribute_selector( 24 | &mut self, 25 | _namespace: &NamespaceConstraint<&>::NamespaceUrl>, 26 | _local_name: &>::LocalName, 27 | _local_name_lower: &>::LocalName, 28 | ) -> bool { 29 | true 30 | } 31 | 32 | /// Visit a simple selector. 33 | fn visit_simple_selector(&mut self, _: &Component<'i, Self::Impl>) -> bool { 34 | true 35 | } 36 | 37 | /// Visit a nested selector list. The caller is responsible to call visit 38 | /// into the internal selectors if / as needed. 39 | /// 40 | /// The default implementation does this. 41 | fn visit_selector_list(&mut self, list: &[Selector<'i, Self::Impl>]) -> bool { 42 | for nested in list { 43 | if !nested.visit(self) { 44 | return false; 45 | } 46 | } 47 | true 48 | } 49 | 50 | /// Visits a complex selector. 51 | /// 52 | /// Gets the combinator to the right of the selector, or `None` if the 53 | /// selector is the rightmost one. 54 | fn visit_complex_selector(&mut self, _combinator_to_right: Option) -> bool { 55 | true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | //! Dependency analysis. 2 | //! 3 | //! Dependencies in CSS can be analyzed using the `analyze_dependencies` option 4 | //! when printing a style sheet. These include other style sheets referenved via 5 | //! the `@import` rule, as well as `url()` references. See [PrinterOptions](PrinterOptions). 6 | //! 7 | //! When dependency analysis is enabled, `@import` rules are removed, and `url()` 8 | //! dependencies are replaced with hashed placeholders that can be substituted with 9 | //! the final urls later (e.g. after bundling and content hashing). 10 | 11 | use crate::css_modules::hash; 12 | use crate::printer::PrinterOptions; 13 | use crate::rules::import::ImportRule; 14 | use crate::traits::ToCss; 15 | use crate::values::url::Url; 16 | #[cfg(feature = "visitor")] 17 | use crate::visitor::Visit; 18 | use cssparser::SourceLocation; 19 | #[cfg(any(feature = "serde", feature = "nodejs"))] 20 | use serde::Serialize; 21 | 22 | /// Options for `analyze_dependencies` in `PrinterOptions`. 23 | #[derive(Default)] 24 | pub struct DependencyOptions { 25 | /// Whether to remove `@import` rules. 26 | pub remove_imports: bool, 27 | } 28 | 29 | /// A dependency. 30 | #[derive(Debug)] 31 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] 32 | #[cfg_attr( 33 | any(feature = "serde", feature = "nodejs"), 34 | serde(tag = "type", rename_all = "lowercase") 35 | )] 36 | pub enum Dependency { 37 | /// An `@import` dependency. 38 | Import(ImportDependency), 39 | /// A `url()` dependency. 40 | Url(UrlDependency), 41 | } 42 | 43 | /// An `@import` dependency. 44 | #[derive(Debug)] 45 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] 46 | pub struct ImportDependency { 47 | /// The url to import. 48 | pub url: String, 49 | /// The placeholder that the URL was replaced with. 50 | pub placeholder: String, 51 | /// An optional `supports()` condition. 52 | pub supports: Option, 53 | /// A media query. 54 | pub media: Option, 55 | /// The location of the dependency in the source file. 56 | pub loc: SourceRange, 57 | } 58 | 59 | impl ImportDependency { 60 | /// Creates a new dependency from an `@import` rule. 61 | pub fn new(rule: &ImportRule, filename: &str) -> ImportDependency { 62 | let supports = if let Some(supports) = &rule.supports { 63 | let s = supports.to_css_string(PrinterOptions::default()).unwrap(); 64 | Some(s) 65 | } else { 66 | None 67 | }; 68 | 69 | let media = if !rule.media.media_queries.is_empty() { 70 | let s = rule.media.to_css_string(PrinterOptions::default()).unwrap(); 71 | Some(s) 72 | } else { 73 | None 74 | }; 75 | 76 | let placeholder = hash(&format!("{}_{}", filename, rule.url), false); 77 | 78 | ImportDependency { 79 | url: rule.url.as_ref().to_owned(), 80 | placeholder, 81 | supports, 82 | media, 83 | loc: SourceRange::new( 84 | filename, 85 | Location { 86 | line: rule.loc.line + 1, 87 | column: rule.loc.column, 88 | }, 89 | 8, 90 | rule.url.len() + 2, 91 | ), // TODO: what about @import url(...)? 92 | } 93 | } 94 | } 95 | 96 | /// A `url()` dependency. 97 | #[derive(Debug)] 98 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] 99 | pub struct UrlDependency { 100 | /// The url of the dependency. 101 | pub url: String, 102 | /// The placeholder that the URL was replaced with. 103 | pub placeholder: String, 104 | /// The location of the dependency in the source file. 105 | pub loc: SourceRange, 106 | } 107 | 108 | impl UrlDependency { 109 | /// Creates a new url dependency. 110 | pub fn new(url: &Url, filename: &str) -> UrlDependency { 111 | let placeholder = hash(&format!("{}_{}", filename, url.url), false); 112 | UrlDependency { 113 | url: url.url.to_string(), 114 | placeholder, 115 | loc: SourceRange::new(filename, url.loc, 4, url.url.len()), 116 | } 117 | } 118 | } 119 | 120 | /// Represents the range of source code where a dependency was found. 121 | #[derive(Debug)] 122 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] 123 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))] 124 | pub struct SourceRange { 125 | /// The filename in which the dependency was found. 126 | pub file_path: String, 127 | /// The starting line and column position of the dependency. 128 | pub start: Location, 129 | /// The ending line and column position of the dependency. 130 | pub end: Location, 131 | } 132 | 133 | /// A line and column position within a source file. 134 | #[derive(Debug, Clone, Copy, PartialEq)] 135 | #[cfg_attr(feature = "visitor", derive(Visit))] 136 | #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))] 137 | #[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))] 138 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 139 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 140 | pub struct Location { 141 | /// The line number, starting from 1. 142 | pub line: u32, 143 | /// The column number, starting from 1. 144 | pub column: u32, 145 | } 146 | 147 | impl From for Location { 148 | fn from(loc: SourceLocation) -> Location { 149 | Location { 150 | line: loc.line + 1, 151 | column: loc.column, 152 | } 153 | } 154 | } 155 | 156 | impl SourceRange { 157 | fn new(filename: &str, loc: Location, offset: u32, len: usize) -> SourceRange { 158 | SourceRange { 159 | file_path: filename.into(), 160 | start: Location { 161 | line: loc.line, 162 | column: loc.column + offset, 163 | }, 164 | end: Location { 165 | line: loc.line, 166 | column: loc.column + offset + (len as u32) - 1, 167 | }, 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/logical.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub enum PropertyCategory { 3 | Logical, 4 | Physical, 5 | } 6 | 7 | impl Default for PropertyCategory { 8 | fn default() -> PropertyCategory { 9 | PropertyCategory::Physical 10 | } 11 | } 12 | 13 | #[derive(PartialEq)] 14 | pub enum LogicalGroup { 15 | BorderColor, 16 | BorderStyle, 17 | BorderWidth, 18 | BorderRadius, 19 | Margin, 20 | ScrollMargin, 21 | Padding, 22 | ScrollPadding, 23 | Inset, 24 | Size, 25 | MinSize, 26 | MaxSize, 27 | } 28 | -------------------------------------------------------------------------------- /src/properties/contain.rs: -------------------------------------------------------------------------------- 1 | //! CSS properties related to containment. 2 | 3 | #![allow(non_upper_case_globals)] 4 | 5 | use cssparser::*; 6 | use smallvec::SmallVec; 7 | 8 | #[cfg(feature = "visitor")] 9 | use crate::visitor::Visit; 10 | use crate::{ 11 | context::PropertyHandlerContext, 12 | declaration::{DeclarationBlock, DeclarationList}, 13 | error::{ParserError, PrinterError}, 14 | macros::{define_shorthand, enum_property, shorthand_handler}, 15 | printer::Printer, 16 | properties::{Property, PropertyId}, 17 | rules::container::ContainerName as ContainerIdent, 18 | targets::Browsers, 19 | traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss}, 20 | }; 21 | 22 | enum_property! { 23 | /// A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property. 24 | /// Establishes the element as a query container for the purpose of container queries. 25 | pub enum ContainerType { 26 | /// The element is not a query container for any container size queries, 27 | /// but remains a query container for container style queries. 28 | Normal, 29 | /// Establishes a query container for container size queries on the container’s own inline axis. 30 | InlineSize, 31 | /// Establishes a query container for container size queries on both the inline and block axis. 32 | Size, 33 | } 34 | } 35 | 36 | impl Default for ContainerType { 37 | fn default() -> Self { 38 | ContainerType::Normal 39 | } 40 | } 41 | 42 | impl IsCompatible for ContainerType { 43 | fn is_compatible(&self, _browsers: Browsers) -> bool { 44 | true 45 | } 46 | } 47 | 48 | /// A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property. 49 | #[derive(Debug, Clone, PartialEq)] 50 | #[cfg_attr(feature = "visitor", derive(Visit))] 51 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 52 | #[cfg_attr( 53 | feature = "serde", 54 | derive(serde::Serialize, serde::Deserialize), 55 | serde(tag = "type", content = "value", rename_all = "kebab-case") 56 | )] 57 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 58 | pub enum ContainerNameList<'i> { 59 | /// The `none` keyword. 60 | None, 61 | /// A list of container names. 62 | #[cfg_attr(feature = "serde", serde(borrow))] 63 | Names(SmallVec<[ContainerIdent<'i>; 1]>), 64 | } 65 | 66 | impl<'i> Default for ContainerNameList<'i> { 67 | fn default() -> Self { 68 | ContainerNameList::None 69 | } 70 | } 71 | 72 | impl<'i> Parse<'i> for ContainerNameList<'i> { 73 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 74 | if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { 75 | return Ok(ContainerNameList::None); 76 | } 77 | 78 | let mut names = SmallVec::new(); 79 | while let Ok(name) = input.try_parse(ContainerIdent::parse) { 80 | names.push(name); 81 | } 82 | 83 | if names.is_empty() { 84 | return Err(input.new_error_for_next_token()); 85 | } else { 86 | return Ok(ContainerNameList::Names(names)); 87 | } 88 | } 89 | } 90 | 91 | impl<'i> ToCss for ContainerNameList<'i> { 92 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 93 | where 94 | W: std::fmt::Write, 95 | { 96 | match self { 97 | ContainerNameList::None => dest.write_str("none"), 98 | ContainerNameList::Names(names) => { 99 | let mut first = true; 100 | for name in names { 101 | if first { 102 | first = false; 103 | } else { 104 | dest.write_char(' ')?; 105 | } 106 | name.to_css(dest)?; 107 | } 108 | Ok(()) 109 | } 110 | } 111 | } 112 | } 113 | 114 | impl IsCompatible for ContainerNameList<'_> { 115 | fn is_compatible(&self, _browsers: Browsers) -> bool { 116 | true 117 | } 118 | } 119 | 120 | define_shorthand! { 121 | /// A value for the [container](https://drafts.csswg.org/css-contain-3/#container-shorthand) shorthand property. 122 | pub struct Container<'i> { 123 | /// The container name. 124 | #[cfg_attr(feature = "serde", serde(borrow))] 125 | name: ContainerName(ContainerNameList<'i>), 126 | /// The container type. 127 | container_type: ContainerType(ContainerType), 128 | } 129 | } 130 | 131 | impl<'i> Parse<'i> for Container<'i> { 132 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 133 | let name = ContainerNameList::parse(input)?; 134 | let container_type = if input.try_parse(|input| input.expect_delim('/')).is_ok() { 135 | ContainerType::parse(input)? 136 | } else { 137 | ContainerType::default() 138 | }; 139 | Ok(Container { name, container_type }) 140 | } 141 | } 142 | 143 | impl<'i> ToCss for Container<'i> { 144 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 145 | where 146 | W: std::fmt::Write, 147 | { 148 | self.name.to_css(dest)?; 149 | if self.container_type != ContainerType::default() { 150 | dest.delim('/', true)?; 151 | self.container_type.to_css(dest)?; 152 | } 153 | Ok(()) 154 | } 155 | } 156 | 157 | shorthand_handler!(ContainerHandler -> Container<'i> { 158 | name: ContainerName(ContainerNameList<'i>), 159 | container_type: ContainerType(ContainerType), 160 | }); 161 | -------------------------------------------------------------------------------- /src/properties/css_modules.rs: -------------------------------------------------------------------------------- 1 | //! Properties related to CSS modules. 2 | 3 | use crate::dependencies::Location; 4 | use crate::error::{ParserError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | use crate::values::ident::{CustomIdent, CustomIdentList}; 8 | use crate::values::string::CowArcStr; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | use cssparser::*; 12 | use smallvec::SmallVec; 13 | 14 | /// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules. 15 | #[derive(Debug, Clone, PartialEq)] 16 | #[cfg_attr(feature = "visitor", derive(Visit))] 17 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 18 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 19 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 20 | pub struct Composes<'i> { 21 | /// A list of class names to compose. 22 | #[cfg_attr(feature = "serde", serde(borrow))] 23 | pub names: CustomIdentList<'i>, 24 | /// Where the class names are composed from. 25 | pub from: Option>, 26 | /// The source location of the `composes` property. 27 | pub loc: Location, 28 | } 29 | 30 | /// Defines where the class names referenced in the `composes` property are located. 31 | /// 32 | /// See [Composes](Composes). 33 | #[derive(Debug, Clone, PartialEq)] 34 | #[cfg_attr(feature = "visitor", derive(Visit))] 35 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 36 | #[cfg_attr( 37 | feature = "serde", 38 | derive(serde::Serialize, serde::Deserialize), 39 | serde(tag = "type", content = "value", rename_all = "kebab-case") 40 | )] 41 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 42 | pub enum Specifier<'i> { 43 | /// The referenced name is global. 44 | Global, 45 | /// The referenced name comes from the specified file. 46 | #[cfg_attr(feature = "serde", serde(borrow))] 47 | File(CowArcStr<'i>), 48 | /// The referenced name comes from a source index (used during bundling). 49 | SourceIndex(u32), 50 | } 51 | 52 | impl<'i> Parse<'i> for Composes<'i> { 53 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 54 | let loc = input.current_source_location(); 55 | let mut names = SmallVec::new(); 56 | while let Ok(name) = input.try_parse(parse_one_ident) { 57 | names.push(name); 58 | } 59 | 60 | if names.is_empty() { 61 | return Err(input.new_custom_error(ParserError::InvalidDeclaration)); 62 | } 63 | 64 | let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() { 65 | Some(Specifier::parse(input)?) 66 | } else { 67 | None 68 | }; 69 | 70 | Ok(Composes { 71 | names, 72 | from, 73 | loc: loc.into(), 74 | }) 75 | } 76 | } 77 | 78 | fn parse_one_ident<'i, 't>( 79 | input: &mut Parser<'i, 't>, 80 | ) -> Result, ParseError<'i, ParserError<'i>>> { 81 | let name = CustomIdent::parse(input)?; 82 | if name.0.eq_ignore_ascii_case("from") { 83 | return Err(input.new_error_for_next_token()); 84 | } 85 | 86 | Ok(name) 87 | } 88 | 89 | impl ToCss for Composes<'_> { 90 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 91 | where 92 | W: std::fmt::Write, 93 | { 94 | let mut first = true; 95 | for name in &self.names { 96 | if first { 97 | first = false; 98 | } else { 99 | dest.write_char(' ')?; 100 | } 101 | name.to_css(dest)?; 102 | } 103 | 104 | if let Some(from) = &self.from { 105 | dest.write_str(" from ")?; 106 | from.to_css(dest)?; 107 | } 108 | 109 | Ok(()) 110 | } 111 | } 112 | 113 | impl<'i> Parse<'i> for Specifier<'i> { 114 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 115 | if let Ok(file) = input.try_parse(|input| input.expect_string_cloned()) { 116 | Ok(Specifier::File(file.into())) 117 | } else { 118 | input.expect_ident_matching("global")?; 119 | Ok(Specifier::Global) 120 | } 121 | } 122 | } 123 | 124 | impl<'i> ToCss for Specifier<'i> { 125 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 126 | where 127 | W: std::fmt::Write, 128 | { 129 | match self { 130 | Specifier::Global => dest.write_str("global")?, 131 | Specifier::File(file) => serialize_string(&file, dest)?, 132 | Specifier::SourceIndex(..) => {} 133 | } 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/properties/outline.rs: -------------------------------------------------------------------------------- 1 | //! CSS properties related to outlines. 2 | 3 | use super::border::{BorderSideWidth, GenericBorder, LineStyle}; 4 | use super::{Property, PropertyId}; 5 | use crate::context::PropertyHandlerContext; 6 | use crate::declaration::{DeclarationBlock, DeclarationList}; 7 | use crate::error::{ParserError, PrinterError}; 8 | use crate::macros::{impl_shorthand, shorthand_handler}; 9 | use crate::printer::Printer; 10 | use crate::targets::Browsers; 11 | use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss}; 12 | use crate::values::color::CssColor; 13 | #[cfg(feature = "visitor")] 14 | use crate::visitor::Visit; 15 | use cssparser::*; 16 | 17 | /// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property. 18 | #[derive(Debug, Clone, PartialEq, Parse, ToCss)] 19 | #[cfg_attr(feature = "visitor", derive(Visit))] 20 | #[cfg_attr( 21 | feature = "serde", 22 | derive(serde::Serialize, serde::Deserialize), 23 | serde(tag = "type", content = "value", rename_all = "kebab-case") 24 | )] 25 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 26 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 27 | pub enum OutlineStyle { 28 | /// The `auto` keyword. 29 | Auto, 30 | /// A value equivalent to the `border-style` property. 31 | LineStyle(LineStyle), 32 | } 33 | 34 | impl Default for OutlineStyle { 35 | fn default() -> OutlineStyle { 36 | OutlineStyle::LineStyle(LineStyle::None) 37 | } 38 | } 39 | 40 | impl IsCompatible for OutlineStyle { 41 | fn is_compatible(&self, _browsers: Browsers) -> bool { 42 | true 43 | } 44 | } 45 | 46 | /// A value for the [outline](https://drafts.csswg.org/css-ui/#outline) shorthand property. 47 | pub type Outline = GenericBorder; 48 | 49 | impl_shorthand! { 50 | Outline(Outline) { 51 | width: [OutlineWidth], 52 | style: [OutlineStyle], 53 | color: [OutlineColor], 54 | } 55 | } 56 | 57 | shorthand_handler!(OutlineHandler -> Outline fallbacks: true { 58 | width: OutlineWidth(BorderSideWidth), 59 | style: OutlineStyle(OutlineStyle), 60 | color: OutlineColor(CssColor, fallback: true), 61 | }); 62 | -------------------------------------------------------------------------------- /src/properties/overflow.rs: -------------------------------------------------------------------------------- 1 | //! CSS properties related to overflow. 2 | 3 | use super::{Property, PropertyId}; 4 | use crate::compat::Feature; 5 | use crate::context::PropertyHandlerContext; 6 | use crate::declaration::{DeclarationBlock, DeclarationList}; 7 | use crate::error::{ParserError, PrinterError}; 8 | use crate::macros::{define_shorthand, enum_property}; 9 | use crate::printer::Printer; 10 | use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss}; 11 | #[cfg(feature = "visitor")] 12 | use crate::visitor::Visit; 13 | use cssparser::*; 14 | 15 | enum_property! { 16 | /// An [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) keyword 17 | /// as used in the `overflow-x`, `overflow-y`, and `overflow` properties. 18 | pub enum OverflowKeyword { 19 | /// Overflowing content is visible. 20 | Visible, 21 | /// Overflowing content is hidden. Programmatic scrolling is allowed. 22 | Hidden, 23 | /// Overflowing content is clipped. Programmatic scrolling is not allowed. 24 | Clip, 25 | /// The element is scrollable. 26 | Scroll, 27 | /// Overflowing content scrolls if needed. 28 | Auto, 29 | } 30 | } 31 | 32 | define_shorthand! { 33 | /// A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property. 34 | pub struct Overflow { 35 | /// The overflow mode for the x direction. 36 | x: OverflowX(OverflowKeyword), 37 | /// The overflow mode for the y direction. 38 | y: OverflowY(OverflowKeyword), 39 | } 40 | } 41 | 42 | impl<'i> Parse<'i> for Overflow { 43 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 44 | let x = OverflowKeyword::parse(input)?; 45 | let y = input.try_parse(OverflowKeyword::parse).unwrap_or_else(|_| x.clone()); 46 | Ok(Overflow { x, y }) 47 | } 48 | } 49 | 50 | impl ToCss for Overflow { 51 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 52 | where 53 | W: std::fmt::Write, 54 | { 55 | self.x.to_css(dest)?; 56 | if self.y != self.x { 57 | dest.write_char(' ')?; 58 | self.y.to_css(dest)?; 59 | } 60 | Ok(()) 61 | } 62 | } 63 | 64 | enum_property! { 65 | /// A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property. 66 | pub enum TextOverflow { 67 | /// Overflowing text is clipped. 68 | Clip, 69 | /// Overflowing text is truncated with an ellipsis. 70 | Ellipsis, 71 | } 72 | } 73 | 74 | #[derive(Default)] 75 | pub(crate) struct OverflowHandler { 76 | x: Option, 77 | y: Option, 78 | } 79 | 80 | impl<'i> PropertyHandler<'i> for OverflowHandler { 81 | fn handle_property( 82 | &mut self, 83 | property: &Property<'i>, 84 | dest: &mut DeclarationList<'i>, 85 | context: &mut PropertyHandlerContext<'i, '_>, 86 | ) -> bool { 87 | use Property::*; 88 | 89 | match property { 90 | OverflowX(val) => self.x = Some(*val), 91 | OverflowY(val) => self.y = Some(*val), 92 | Overflow(val) => { 93 | self.x = Some(val.x); 94 | self.y = Some(val.y); 95 | } 96 | Unparsed(val) 97 | if matches!( 98 | val.property_id, 99 | PropertyId::OverflowX | PropertyId::OverflowY | PropertyId::Overflow 100 | ) => 101 | { 102 | self.finalize(dest, context); 103 | dest.push(property.clone()); 104 | } 105 | _ => return false, 106 | } 107 | 108 | true 109 | } 110 | 111 | fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) { 112 | if self.x.is_none() && self.y.is_none() { 113 | return; 114 | } 115 | 116 | let x = std::mem::take(&mut self.x); 117 | let y = std::mem::take(&mut self.y); 118 | 119 | match (x, y) { 120 | // Only use shorthand syntax if the x and y values are the 121 | // same or the two-value syntax is supported by all targets. 122 | (Some(x), Some(y)) if x == y || context.targets.is_compatible(Feature::OverflowShorthand) => { 123 | dest.push(Property::Overflow(Overflow { x, y })) 124 | } 125 | _ => { 126 | if let Some(x) = x { 127 | dest.push(Property::OverflowX(x)) 128 | } 129 | 130 | if let Some(y) = y { 131 | dest.push(Property::OverflowY(y)) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/properties/position.rs: -------------------------------------------------------------------------------- 1 | //! CSS properties related to positioning. 2 | 3 | use super::Property; 4 | use crate::context::PropertyHandlerContext; 5 | use crate::declaration::DeclarationList; 6 | use crate::error::{ParserError, PrinterError}; 7 | use crate::prefixes::Feature; 8 | use crate::printer::Printer; 9 | use crate::traits::{Parse, PropertyHandler, ToCss}; 10 | use crate::values::number::CSSInteger; 11 | use crate::vendor_prefix::VendorPrefix; 12 | #[cfg(feature = "visitor")] 13 | use crate::visitor::Visit; 14 | use cssparser::*; 15 | 16 | /// A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property. 17 | #[derive(Debug, Clone, PartialEq)] 18 | #[cfg_attr(feature = "visitor", derive(Visit))] 19 | #[cfg_attr( 20 | feature = "serde", 21 | derive(serde::Serialize, serde::Deserialize), 22 | serde(tag = "type", content = "value", rename_all = "kebab-case") 23 | )] 24 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 25 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 26 | pub enum Position { 27 | /// The box is laid in the document flow. 28 | Static, 29 | /// The box is laid out in the document flow and offset from the resulting position. 30 | Relative, 31 | /// The box is taken out of document flow and positioned in reference to its relative ancestor. 32 | Absolute, 33 | /// Similar to relative but adjusted according to the ancestor scrollable element. 34 | Sticky(VendorPrefix), 35 | /// The box is taken out of the document flow and positioned in reference to the page viewport. 36 | Fixed, 37 | } 38 | 39 | impl<'i> Parse<'i> for Position { 40 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 41 | let location = input.current_source_location(); 42 | let ident = input.expect_ident()?; 43 | match_ignore_ascii_case! { &*ident, 44 | "static" => Ok(Position::Static), 45 | "relative" => Ok(Position::Relative), 46 | "absolute" => Ok(Position::Absolute), 47 | "fixed" => Ok(Position::Fixed), 48 | "sticky" => Ok(Position::Sticky(VendorPrefix::None)), 49 | "-webkit-sticky" => Ok(Position::Sticky(VendorPrefix::WebKit)), 50 | _ => Err(location.new_unexpected_token_error( 51 | cssparser::Token::Ident(ident.clone()) 52 | )) 53 | } 54 | } 55 | } 56 | 57 | impl ToCss for Position { 58 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 59 | where 60 | W: std::fmt::Write, 61 | { 62 | match self { 63 | Position::Static => dest.write_str("static"), 64 | Position::Relative => dest.write_str("relative"), 65 | Position::Absolute => dest.write_str("absolute"), 66 | Position::Fixed => dest.write_str("fixed"), 67 | Position::Sticky(prefix) => { 68 | prefix.to_css(dest)?; 69 | dest.write_str("sticky") 70 | } 71 | } 72 | } 73 | } 74 | 75 | /// A value for the [z-index](https://drafts.csswg.org/css2/#z-index) property. 76 | #[derive(Debug, Clone, PartialEq, Parse, ToCss)] 77 | #[cfg_attr(feature = "visitor", derive(Visit))] 78 | #[cfg_attr( 79 | feature = "serde", 80 | derive(serde::Serialize, serde::Deserialize), 81 | serde(tag = "type", content = "value", rename_all = "kebab-case") 82 | )] 83 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 84 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 85 | pub enum ZIndex { 86 | /// The `auto` keyword. 87 | Auto, 88 | /// An integer value. 89 | Integer(CSSInteger), 90 | } 91 | 92 | #[derive(Default)] 93 | pub(crate) struct PositionHandler { 94 | position: Option, 95 | } 96 | 97 | impl<'i> PropertyHandler<'i> for PositionHandler { 98 | fn handle_property( 99 | &mut self, 100 | property: &Property<'i>, 101 | _: &mut DeclarationList<'i>, 102 | _: &mut PropertyHandlerContext<'i, '_>, 103 | ) -> bool { 104 | if let Property::Position(position) = property { 105 | if let (Some(Position::Sticky(cur)), Position::Sticky(new)) = (&mut self.position, position) { 106 | *cur |= *new; 107 | } else { 108 | self.position = Some(position.clone()); 109 | } 110 | 111 | return true; 112 | } 113 | 114 | false 115 | } 116 | 117 | fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) { 118 | if self.position.is_none() { 119 | return; 120 | } 121 | 122 | if let Some(position) = std::mem::take(&mut self.position) { 123 | match position { 124 | Position::Sticky(mut prefix) => { 125 | prefix = context.targets.prefixes(prefix, Feature::Sticky); 126 | if prefix.contains(VendorPrefix::WebKit) { 127 | dest.push(Property::Position(Position::Sticky(VendorPrefix::WebKit))) 128 | } 129 | 130 | if prefix.contains(VendorPrefix::None) { 131 | dest.push(Property::Position(Position::Sticky(VendorPrefix::None))) 132 | } 133 | } 134 | _ => dest.push(Property::Position(position)), 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/rules/counter_style.rs: -------------------------------------------------------------------------------- 1 | //! The `@counter-style` rule. 2 | 3 | use super::Location; 4 | use crate::declaration::DeclarationBlock; 5 | use crate::error::PrinterError; 6 | use crate::printer::Printer; 7 | use crate::traits::ToCss; 8 | use crate::values::ident::CustomIdent; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@counter-style](https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule) rule. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | pub struct CounterStyleRule<'i> { 19 | /// The name of the counter style to declare. 20 | #[cfg_attr(feature = "serde", serde(borrow))] 21 | pub name: CustomIdent<'i>, 22 | // TODO: eventually parse these properties 23 | /// Declarations in the `@counter-style` rule. 24 | pub declarations: DeclarationBlock<'i>, 25 | /// The location of the rule in the source file. 26 | #[cfg_attr(feature = "visitor", skip_visit)] 27 | pub loc: Location, 28 | } 29 | 30 | impl<'i> ToCss for CounterStyleRule<'i> { 31 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 32 | where 33 | W: std::fmt::Write, 34 | { 35 | #[cfg(feature = "sourcemap")] 36 | dest.add_mapping(self.loc); 37 | dest.write_str("@counter-style ")?; 38 | self.name.to_css(dest)?; 39 | self.declarations.to_css_block(dest) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/rules/custom_media.rs: -------------------------------------------------------------------------------- 1 | //! The `@custom-media` rule. 2 | 3 | use super::Location; 4 | use crate::error::PrinterError; 5 | use crate::media_query::MediaList; 6 | use crate::printer::Printer; 7 | use crate::traits::ToCss; 8 | use crate::values::ident::DashedIdent; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@custom-media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) rule. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | pub struct CustomMediaRule<'i> { 19 | /// The name of the declared media query. 20 | #[cfg_attr(feature = "serde", serde(borrow))] 21 | pub name: DashedIdent<'i>, 22 | /// The media query to declare. 23 | pub query: MediaList<'i>, 24 | /// The location of the rule in the source file. 25 | #[cfg_attr(feature = "visitor", skip_visit)] 26 | pub loc: Location, 27 | } 28 | 29 | impl<'i> ToCss for CustomMediaRule<'i> { 30 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 31 | where 32 | W: std::fmt::Write, 33 | { 34 | #[cfg(feature = "sourcemap")] 35 | dest.add_mapping(self.loc); 36 | dest.write_str("@custom-media ")?; 37 | self.name.to_css(dest)?; 38 | dest.write_char(' ')?; 39 | self.query.to_css(dest)?; 40 | dest.write_char(';') 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/rules/document.rs: -------------------------------------------------------------------------------- 1 | //! The `@-moz-document` rule. 2 | 3 | use super::Location; 4 | use super::{CssRuleList, MinifyContext}; 5 | use crate::error::{MinifyError, PrinterError}; 6 | use crate::parser::DefaultAtRule; 7 | use crate::printer::Printer; 8 | use crate::traits::ToCss; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@-moz-document](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) rule. 13 | /// 14 | /// Note that only the `url-prefix()` function with no arguments is supported, and only the `-moz` prefix 15 | /// is allowed since Firefox was the only browser that ever implemented this rule. 16 | #[derive(Debug, PartialEq, Clone)] 17 | #[cfg_attr(feature = "visitor", derive(Visit))] 18 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 19 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 20 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 21 | pub struct MozDocumentRule<'i, R = DefaultAtRule> { 22 | /// Nested rules within the `@-moz-document` rule. 23 | #[cfg_attr(feature = "serde", serde(borrow))] 24 | pub rules: CssRuleList<'i, R>, 25 | /// The location of the rule in the source file. 26 | #[cfg_attr(feature = "visitor", skip_visit)] 27 | pub loc: Location, 28 | } 29 | 30 | impl<'i, T: Clone> MozDocumentRule<'i, T> { 31 | pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> { 32 | self.rules.minify(context, false) 33 | } 34 | } 35 | 36 | impl<'i, T: ToCss> ToCss for MozDocumentRule<'i, T> { 37 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 38 | where 39 | W: std::fmt::Write, 40 | { 41 | #[cfg(feature = "sourcemap")] 42 | dest.add_mapping(self.loc); 43 | dest.write_str("@-moz-document url-prefix()")?; 44 | dest.whitespace()?; 45 | dest.write_char('{')?; 46 | dest.indent(); 47 | dest.newline()?; 48 | self.rules.to_css(dest)?; 49 | dest.dedent(); 50 | dest.newline()?; 51 | dest.write_char('}') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/rules/import.rs: -------------------------------------------------------------------------------- 1 | //! The `@import` rule. 2 | 3 | use super::layer::LayerName; 4 | use super::supports::SupportsCondition; 5 | use super::Location; 6 | use crate::dependencies::{Dependency, ImportDependency}; 7 | use crate::error::PrinterError; 8 | use crate::media_query::MediaList; 9 | use crate::printer::Printer; 10 | use crate::traits::ToCss; 11 | use crate::values::string::CowArcStr; 12 | #[cfg(feature = "visitor")] 13 | use crate::visitor::Visit; 14 | use cssparser::*; 15 | 16 | /// A [@import](https://drafts.csswg.org/css-cascade/#at-import) rule. 17 | #[derive(Debug, PartialEq, Clone)] 18 | #[cfg_attr(feature = "visitor", derive(Visit))] 19 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 20 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 21 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 22 | pub struct ImportRule<'i> { 23 | /// The url to import. 24 | #[cfg_attr(feature = "serde", serde(borrow))] 25 | #[cfg_attr(feature = "visitor", skip_visit)] 26 | pub url: CowArcStr<'i>, 27 | /// An optional cascade layer name, or `None` for an anonymous layer. 28 | #[cfg_attr(feature = "visitor", skip_visit)] 29 | pub layer: Option>>, 30 | /// An optional `supports()` condition. 31 | pub supports: Option>, 32 | /// A media query. 33 | #[cfg_attr(feature = "serde", serde(default))] 34 | pub media: MediaList<'i>, 35 | /// The location of the rule in the source file. 36 | #[cfg_attr(feature = "visitor", skip_visit)] 37 | pub loc: Location, 38 | } 39 | 40 | impl<'i> ToCss for ImportRule<'i> { 41 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 42 | where 43 | W: std::fmt::Write, 44 | { 45 | let dep = if dest.dependencies.is_some() { 46 | Some(ImportDependency::new(self, dest.filename())) 47 | } else { 48 | None 49 | }; 50 | 51 | #[cfg(feature = "sourcemap")] 52 | dest.add_mapping(self.loc); 53 | dest.write_str("@import ")?; 54 | if let Some(dep) = dep { 55 | serialize_string(&dep.placeholder, dest)?; 56 | 57 | if let Some(dependencies) = &mut dest.dependencies { 58 | dependencies.push(Dependency::Import(dep)) 59 | } 60 | } else { 61 | serialize_string(&self.url, dest)?; 62 | } 63 | 64 | if let Some(layer) = &self.layer { 65 | dest.write_str(" layer")?; 66 | if let Some(name) = layer { 67 | dest.write_char('(')?; 68 | name.to_css(dest)?; 69 | dest.write_char(')')?; 70 | } 71 | } 72 | 73 | if let Some(supports) = &self.supports { 74 | dest.write_str(" supports")?; 75 | if matches!(supports, SupportsCondition::Declaration { .. }) { 76 | supports.to_css(dest)?; 77 | } else { 78 | dest.write_char('(')?; 79 | supports.to_css(dest)?; 80 | dest.write_char(')')?; 81 | } 82 | } 83 | if !self.media.media_queries.is_empty() { 84 | dest.write_char(' ')?; 85 | self.media.to_css(dest)?; 86 | } 87 | dest.write_str(";") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/rules/layer.rs: -------------------------------------------------------------------------------- 1 | //! The `@layer` rule. 2 | 3 | use super::{CssRuleList, Location, MinifyContext}; 4 | use crate::error::{MinifyError, ParserError, PrinterError}; 5 | use crate::parser::DefaultAtRule; 6 | use crate::printer::Printer; 7 | use crate::traits::{Parse, ToCss}; 8 | use crate::values::string::CowArcStr; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | use cssparser::*; 12 | use smallvec::SmallVec; 13 | 14 | /// A [``](https://drafts.csswg.org/css-cascade-5/#typedef-layer-name) within 15 | /// a `@layer` or `@import` rule. 16 | /// 17 | /// Nested layers are represented using a list of identifiers. In CSS syntax, these are dot-separated. 18 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 19 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 20 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] 21 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 22 | pub struct LayerName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub SmallVec<[CowArcStr<'i>; 1]>); 23 | 24 | macro_rules! expect_non_whitespace { 25 | ($parser: ident, $($branches: tt)+) => {{ 26 | let start_location = $parser.current_source_location(); 27 | match *$parser.next_including_whitespace()? { 28 | $($branches)+ 29 | ref token => { 30 | return Err(start_location.new_basic_unexpected_token_error(token.clone())) 31 | } 32 | } 33 | }} 34 | } 35 | 36 | impl<'i> Parse<'i> for LayerName<'i> { 37 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 38 | let mut parts = SmallVec::new(); 39 | let ident = input.expect_ident()?; 40 | parts.push(ident.into()); 41 | 42 | loop { 43 | let name = input.try_parse(|input| { 44 | expect_non_whitespace! {input, 45 | Token::Delim('.') => Ok(()), 46 | }?; 47 | 48 | expect_non_whitespace! {input, 49 | Token::Ident(ref id) => Ok(id.into()), 50 | } 51 | }); 52 | 53 | match name { 54 | Ok(name) => parts.push(name), 55 | Err(_) => break, 56 | } 57 | } 58 | 59 | Ok(LayerName(parts)) 60 | } 61 | } 62 | 63 | impl<'i> ToCss for LayerName<'i> { 64 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 65 | where 66 | W: std::fmt::Write, 67 | { 68 | let mut first = true; 69 | for name in &self.0 { 70 | if first { 71 | first = false; 72 | } else { 73 | dest.write_char('.')?; 74 | } 75 | 76 | serialize_identifier(name, dest)?; 77 | } 78 | 79 | Ok(()) 80 | } 81 | } 82 | 83 | /// A [@layer statement](https://drafts.csswg.org/css-cascade-5/#layer-empty) rule. 84 | /// 85 | /// See also [LayerBlockRule](LayerBlockRule). 86 | #[derive(Debug, Clone, PartialEq)] 87 | #[cfg_attr(feature = "visitor", derive(Visit))] 88 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 89 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 90 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 91 | pub struct LayerStatementRule<'i> { 92 | /// The layer names to declare. 93 | #[cfg_attr(feature = "serde", serde(borrow))] 94 | #[cfg_attr(feature = "visitor", skip_visit)] 95 | pub names: Vec>, 96 | /// The location of the rule in the source file. 97 | #[cfg_attr(feature = "visitor", skip_visit)] 98 | pub loc: Location, 99 | } 100 | 101 | impl<'i> ToCss for LayerStatementRule<'i> { 102 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 103 | where 104 | W: std::fmt::Write, 105 | { 106 | #[cfg(feature = "sourcemap")] 107 | dest.add_mapping(self.loc); 108 | dest.write_str("@layer ")?; 109 | self.names.to_css(dest)?; 110 | dest.write_char(';') 111 | } 112 | } 113 | 114 | /// A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule. 115 | #[derive(Debug, Clone, PartialEq)] 116 | #[cfg_attr(feature = "visitor", derive(Visit))] 117 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 118 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 119 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 120 | pub struct LayerBlockRule<'i, R = DefaultAtRule> { 121 | /// The name of the layer to declare, or `None` to declare an anonymous layer. 122 | #[cfg_attr(feature = "serde", serde(borrow))] 123 | #[cfg_attr(feature = "visitor", skip_visit)] 124 | pub name: Option>, 125 | /// The rules within the `@layer` rule. 126 | pub rules: CssRuleList<'i, R>, 127 | /// The location of the rule in the source file. 128 | #[cfg_attr(feature = "visitor", skip_visit)] 129 | pub loc: Location, 130 | } 131 | 132 | impl<'i, T: Clone> LayerBlockRule<'i, T> { 133 | pub(crate) fn minify( 134 | &mut self, 135 | context: &mut MinifyContext<'_, 'i>, 136 | parent_is_unused: bool, 137 | ) -> Result { 138 | self.rules.minify(context, parent_is_unused)?; 139 | 140 | Ok(self.rules.0.is_empty()) 141 | } 142 | } 143 | 144 | impl<'a, 'i, T: ToCss> ToCss for LayerBlockRule<'i, T> { 145 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 146 | where 147 | W: std::fmt::Write, 148 | { 149 | #[cfg(feature = "sourcemap")] 150 | dest.add_mapping(self.loc); 151 | dest.write_str("@layer")?; 152 | if let Some(name) = &self.name { 153 | dest.write_char(' ')?; 154 | name.to_css(dest)?; 155 | } 156 | 157 | dest.whitespace()?; 158 | dest.write_char('{')?; 159 | dest.indent(); 160 | dest.newline()?; 161 | self.rules.to_css(dest)?; 162 | dest.dedent(); 163 | dest.newline()?; 164 | dest.write_char('}') 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/rules/media.rs: -------------------------------------------------------------------------------- 1 | //! The `@media` rule. 2 | 3 | use super::Location; 4 | use super::{CssRuleList, MinifyContext}; 5 | use crate::error::{MinifyError, PrinterError}; 6 | use crate::media_query::MediaList; 7 | use crate::parser::DefaultAtRule; 8 | use crate::printer::Printer; 9 | use crate::traits::ToCss; 10 | #[cfg(feature = "visitor")] 11 | use crate::visitor::Visit; 12 | 13 | /// A [@media](https://drafts.csswg.org/css-conditional-3/#at-media) rule. 14 | #[derive(Debug, PartialEq, Clone)] 15 | #[cfg_attr(feature = "visitor", derive(Visit))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 19 | pub struct MediaRule<'i, R = DefaultAtRule> { 20 | /// The media query list. 21 | #[cfg_attr(feature = "serde", serde(borrow))] 22 | pub query: MediaList<'i>, 23 | /// The rules within the `@media` rule. 24 | pub rules: CssRuleList<'i, R>, 25 | /// The location of the rule in the source file. 26 | #[cfg_attr(feature = "visitor", skip_visit)] 27 | pub loc: Location, 28 | } 29 | 30 | impl<'i, T: Clone> MediaRule<'i, T> { 31 | pub(crate) fn minify( 32 | &mut self, 33 | context: &mut MinifyContext<'_, 'i>, 34 | parent_is_unused: bool, 35 | ) -> Result { 36 | self.rules.minify(context, parent_is_unused)?; 37 | 38 | if let Some(custom_media) = &context.custom_media { 39 | self.query.transform_custom_media(self.loc, custom_media)?; 40 | } 41 | 42 | self.query.transform_resolution(context.targets.current); 43 | Ok(self.rules.0.is_empty() || self.query.never_matches()) 44 | } 45 | } 46 | 47 | impl<'a, 'i, T: ToCss> ToCss for MediaRule<'i, T> { 48 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 49 | where 50 | W: std::fmt::Write, 51 | { 52 | // If the media query always matches, we can just output the nested rules. 53 | if dest.minify && self.query.always_matches() { 54 | self.rules.to_css(dest)?; 55 | return Ok(()); 56 | } 57 | 58 | #[cfg(feature = "sourcemap")] 59 | dest.add_mapping(self.loc); 60 | dest.write_str("@media ")?; 61 | self.query.to_css(dest)?; 62 | dest.whitespace()?; 63 | dest.write_char('{')?; 64 | dest.indent(); 65 | dest.newline()?; 66 | self.rules.to_css(dest)?; 67 | dest.dedent(); 68 | dest.newline()?; 69 | dest.write_char('}') 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/rules/namespace.rs: -------------------------------------------------------------------------------- 1 | //! The `@namespace` rule. 2 | 3 | use super::Location; 4 | use crate::error::PrinterError; 5 | use crate::printer::Printer; 6 | use crate::traits::ToCss; 7 | use crate::values::ident::Ident; 8 | use crate::values::string::CSSString; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@namespace](https://drafts.csswg.org/css-namespaces/#declaration) rule. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | pub struct NamespaceRule<'i> { 19 | /// An optional namespace prefix to declare, or `None` to declare the default namespace. 20 | #[cfg_attr(feature = "serde", serde(borrow))] 21 | #[cfg_attr(feature = "visitor", skip_visit)] 22 | pub prefix: Option>, 23 | /// The url of the namespace. 24 | #[cfg_attr(feature = "serde", serde(borrow))] 25 | #[cfg_attr(feature = "visitor", skip_visit)] 26 | pub url: CSSString<'i>, 27 | /// The location of the rule in the source file. 28 | #[cfg_attr(feature = "visitor", skip_visit)] 29 | pub loc: Location, 30 | } 31 | 32 | impl<'i> ToCss for NamespaceRule<'i> { 33 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 34 | where 35 | W: std::fmt::Write, 36 | { 37 | #[cfg(feature = "sourcemap")] 38 | dest.add_mapping(self.loc); 39 | dest.write_str("@namespace ")?; 40 | if let Some(prefix) = &self.prefix { 41 | prefix.to_css(dest)?; 42 | dest.write_char(' ')?; 43 | } 44 | 45 | self.url.to_css(dest)?; 46 | dest.write_char(';') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/rules/nesting.rs: -------------------------------------------------------------------------------- 1 | //! The `@nest` rule. 2 | 3 | use smallvec::SmallVec; 4 | 5 | use super::style::StyleRule; 6 | use super::Location; 7 | use super::MinifyContext; 8 | use crate::context::DeclarationContext; 9 | use crate::declaration::DeclarationBlock; 10 | use crate::error::{MinifyError, PrinterError}; 11 | use crate::parser::DefaultAtRule; 12 | use crate::printer::Printer; 13 | use crate::targets::should_compile; 14 | use crate::traits::ToCss; 15 | #[cfg(feature = "visitor")] 16 | use crate::visitor::Visit; 17 | 18 | /// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule. 19 | #[derive(Debug, PartialEq, Clone)] 20 | #[cfg_attr(feature = "visitor", derive(Visit))] 21 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 22 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 23 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 24 | pub struct NestingRule<'i, R = DefaultAtRule> { 25 | /// The style rule that defines the selector and declarations for the `@nest` rule. 26 | #[cfg_attr(feature = "serde", serde(borrow))] 27 | pub style: StyleRule<'i, R>, 28 | /// The location of the rule in the source file. 29 | #[cfg_attr(feature = "visitor", skip_visit)] 30 | pub loc: Location, 31 | } 32 | 33 | impl<'i, T: Clone> NestingRule<'i, T> { 34 | pub(crate) fn minify( 35 | &mut self, 36 | context: &mut MinifyContext<'_, 'i>, 37 | parent_is_unused: bool, 38 | ) -> Result { 39 | self.style.minify(context, parent_is_unused) 40 | } 41 | } 42 | 43 | impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { 44 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 45 | where 46 | W: std::fmt::Write, 47 | { 48 | #[cfg(feature = "sourcemap")] 49 | dest.add_mapping(self.loc); 50 | if dest.context().is_none() { 51 | dest.write_str("@nest ")?; 52 | } 53 | self.style.to_css(dest) 54 | } 55 | } 56 | 57 | /// A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule. 58 | #[derive(Debug, PartialEq, Clone)] 59 | #[cfg_attr(feature = "visitor", derive(Visit))] 60 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 61 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 62 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 63 | pub struct NestedDeclarationsRule<'i> { 64 | /// The style rule that defines the selector and declarations for the `@nest` rule. 65 | #[cfg_attr(feature = "serde", serde(borrow))] 66 | pub declarations: DeclarationBlock<'i>, 67 | /// The location of the rule in the source file. 68 | #[cfg_attr(feature = "visitor", skip_visit)] 69 | pub loc: Location, 70 | } 71 | 72 | impl<'i> NestedDeclarationsRule<'i> { 73 | pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool) -> bool { 74 | if parent_is_unused { 75 | return true; 76 | } 77 | 78 | context.handler_context.context = DeclarationContext::StyleRule; 79 | self 80 | .declarations 81 | .minify(context.handler, context.important_handler, &mut context.handler_context); 82 | context.handler_context.context = DeclarationContext::None; 83 | return false; 84 | } 85 | } 86 | 87 | impl<'i> ToCss for NestedDeclarationsRule<'i> { 88 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 89 | where 90 | W: std::fmt::Write, 91 | { 92 | #[cfg(feature = "sourcemap")] 93 | dest.add_mapping(self.loc); 94 | 95 | if should_compile!(dest.targets.current, Nesting) { 96 | if let Some(context) = dest.context() { 97 | let has_printable_declarations = self.declarations.has_printable_declarations(); 98 | if has_printable_declarations { 99 | dest.with_parent_context(|dest| context.selectors.to_css(dest))?; 100 | dest.whitespace()?; 101 | dest.write_char('{')?; 102 | dest.indent(); 103 | dest.newline()?; 104 | } 105 | 106 | self 107 | .declarations 108 | .to_css_declarations(dest, false, &context.selectors, self.loc.source_index)?; 109 | 110 | if has_printable_declarations { 111 | dest.dedent(); 112 | dest.newline()?; 113 | dest.write_char('}')?; 114 | } 115 | return Ok(()); 116 | } 117 | } 118 | 119 | self 120 | .declarations 121 | .to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/rules/scope.rs: -------------------------------------------------------------------------------- 1 | //! The `@scope` rule. 2 | 3 | use super::Location; 4 | use super::{CssRuleList, MinifyContext}; 5 | use crate::error::{MinifyError, PrinterError}; 6 | use crate::parser::DefaultAtRule; 7 | use crate::printer::Printer; 8 | use crate::selector::{is_pure_css_modules_selector, SelectorList}; 9 | use crate::traits::ToCss; 10 | #[cfg(feature = "visitor")] 11 | use crate::visitor::Visit; 12 | 13 | /// A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule. 14 | /// 15 | /// @scope () [to ()]? { 16 | /// 17 | /// } 18 | #[derive(Debug, PartialEq, Clone)] 19 | #[cfg_attr(feature = "visitor", derive(Visit))] 20 | #[cfg_attr( 21 | feature = "serde", 22 | derive(serde::Serialize, serde::Deserialize), 23 | serde(rename_all = "camelCase") 24 | )] 25 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 26 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 27 | pub struct ScopeRule<'i, R = DefaultAtRule> { 28 | /// A selector list used to identify the scoping root(s). 29 | pub scope_start: Option>, 30 | /// A selector list used to identify any scoping limits. 31 | pub scope_end: Option>, 32 | /// Nested rules within the `@scope` rule. 33 | #[cfg_attr(feature = "serde", serde(borrow))] 34 | pub rules: CssRuleList<'i, R>, 35 | /// The location of the rule in the source file. 36 | #[cfg_attr(feature = "visitor", skip_visit)] 37 | pub loc: Location, 38 | } 39 | 40 | impl<'i, T: Clone> ScopeRule<'i, T> { 41 | pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> { 42 | if context.pure_css_modules { 43 | if let Some(scope_start) = &self.scope_start { 44 | if !scope_start.0.iter().all(is_pure_css_modules_selector) { 45 | return Err(MinifyError { 46 | kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, 47 | loc: self.loc, 48 | }); 49 | } 50 | } 51 | 52 | if let Some(scope_end) = &self.scope_end { 53 | if !scope_end.0.iter().all(is_pure_css_modules_selector) { 54 | return Err(MinifyError { 55 | kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector, 56 | loc: self.loc, 57 | }); 58 | } 59 | } 60 | } 61 | 62 | self.rules.minify(context, false) 63 | } 64 | } 65 | 66 | impl<'i, T: ToCss> ToCss for ScopeRule<'i, T> { 67 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 68 | where 69 | W: std::fmt::Write, 70 | { 71 | #[cfg(feature = "sourcemap")] 72 | dest.add_mapping(self.loc); 73 | dest.write_str("@scope")?; 74 | dest.whitespace()?; 75 | if let Some(scope_start) = &self.scope_start { 76 | dest.write_char('(')?; 77 | scope_start.to_css(dest)?; 78 | dest.write_char(')')?; 79 | dest.whitespace()?; 80 | } 81 | if let Some(scope_end) = &self.scope_end { 82 | if dest.minify { 83 | dest.write_char(' ')?; 84 | } 85 | dest.write_str("to (")?; 86 | // is treated as an ancestor of scope end. 87 | // https://drafts.csswg.org/css-nesting/#nesting-at-scope 88 | if let Some(scope_start) = &self.scope_start { 89 | dest.with_context(scope_start, |dest| scope_end.to_css(dest))?; 90 | } else { 91 | scope_end.to_css(dest)?; 92 | } 93 | dest.write_char(')')?; 94 | dest.whitespace()?; 95 | } 96 | dest.write_char('{')?; 97 | dest.indent(); 98 | dest.newline()?; 99 | // Nested style rules within @scope are implicitly relative to the 100 | // so clear our style context while printing them to avoid replacing & ourselves. 101 | // https://drafts.csswg.org/css-cascade-6/#scoped-rules 102 | dest.with_cleared_context(|dest| self.rules.to_css(dest))?; 103 | dest.dedent(); 104 | dest.newline()?; 105 | dest.write_char('}') 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/rules/starting_style.rs: -------------------------------------------------------------------------------- 1 | //! The `@starting-style` rule. 2 | 3 | use super::Location; 4 | use super::{CssRuleList, MinifyContext}; 5 | use crate::error::{MinifyError, PrinterError}; 6 | use crate::parser::DefaultAtRule; 7 | use crate::printer::Printer; 8 | use crate::traits::ToCss; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 16 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 17 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 18 | pub struct StartingStyleRule<'i, R = DefaultAtRule> { 19 | /// Nested rules within the `@starting-style` rule. 20 | #[cfg_attr(feature = "serde", serde(borrow))] 21 | pub rules: CssRuleList<'i, R>, 22 | /// The location of the rule in the source file. 23 | #[cfg_attr(feature = "visitor", skip_visit)] 24 | pub loc: Location, 25 | } 26 | 27 | impl<'i, T: Clone> StartingStyleRule<'i, T> { 28 | pub(crate) fn minify( 29 | &mut self, 30 | context: &mut MinifyContext<'_, 'i>, 31 | parent_is_unused: bool, 32 | ) -> Result { 33 | self.rules.minify(context, parent_is_unused)?; 34 | Ok(self.rules.0.is_empty()) 35 | } 36 | } 37 | 38 | impl<'i, T: ToCss> ToCss for StartingStyleRule<'i, T> { 39 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 40 | where 41 | W: std::fmt::Write, 42 | { 43 | #[cfg(feature = "sourcemap")] 44 | dest.add_mapping(self.loc); 45 | dest.write_str("@starting-style")?; 46 | dest.whitespace()?; 47 | dest.write_char('{')?; 48 | dest.indent(); 49 | dest.newline()?; 50 | self.rules.to_css(dest)?; 51 | dest.dedent(); 52 | dest.newline()?; 53 | dest.write_char('}') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/rules/unknown.rs: -------------------------------------------------------------------------------- 1 | //! An unknown at-rule. 2 | 3 | use super::Location; 4 | use crate::error::PrinterError; 5 | use crate::printer::Printer; 6 | use crate::properties::custom::TokenList; 7 | use crate::traits::ToCss; 8 | use crate::values::string::CowArcStr; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// An unknown at-rule, stored as raw tokens. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | pub struct UnknownAtRule<'i> { 19 | /// The name of the at-rule (without the @). 20 | #[cfg_attr(feature = "serde", serde(borrow))] 21 | #[cfg_attr(feature = "visitor", skip_visit)] 22 | pub name: CowArcStr<'i>, 23 | /// The prelude of the rule. 24 | pub prelude: TokenList<'i>, 25 | /// The contents of the block, if any. 26 | pub block: Option>, 27 | /// The location of the rule in the source file. 28 | #[cfg_attr(feature = "visitor", skip_visit)] 29 | pub loc: Location, 30 | } 31 | 32 | impl<'i> ToCss for UnknownAtRule<'i> { 33 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 34 | where 35 | W: std::fmt::Write, 36 | { 37 | #[cfg(feature = "sourcemap")] 38 | dest.add_mapping(self.loc); 39 | dest.write_char('@')?; 40 | dest.write_str(&self.name)?; 41 | 42 | if !self.prelude.0.is_empty() { 43 | dest.write_char(' ')?; 44 | self.prelude.to_css(dest, false)?; 45 | } 46 | 47 | if let Some(block) = &self.block { 48 | dest.whitespace()?; 49 | dest.write_char('{')?; 50 | dest.indent(); 51 | dest.newline()?; 52 | block.to_css(dest, false)?; 53 | dest.dedent(); 54 | dest.newline()?; 55 | dest.write_char('}') 56 | } else { 57 | dest.write_char(';') 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/rules/viewport.rs: -------------------------------------------------------------------------------- 1 | //! The `@viewport` rule. 2 | 3 | use super::Location; 4 | use crate::declaration::DeclarationBlock; 5 | use crate::error::PrinterError; 6 | use crate::printer::Printer; 7 | use crate::traits::ToCss; 8 | use crate::vendor_prefix::VendorPrefix; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | 12 | /// A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule. 13 | #[derive(Debug, PartialEq, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr( 17 | feature = "serde", 18 | derive(serde::Serialize, serde::Deserialize), 19 | serde(rename_all = "camelCase") 20 | )] 21 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 22 | pub struct ViewportRule<'i> { 23 | /// The vendor prefix for this rule, e.g. `@-ms-viewport`. 24 | #[cfg_attr(feature = "visitor", skip_visit)] 25 | pub vendor_prefix: VendorPrefix, 26 | /// The declarations within the `@viewport` rule. 27 | #[cfg_attr(feature = "serde", serde(borrow))] 28 | pub declarations: DeclarationBlock<'i>, 29 | /// The location of the rule in the source file. 30 | #[cfg_attr(feature = "visitor", skip_visit)] 31 | pub loc: Location, 32 | } 33 | 34 | impl<'i> ToCss for ViewportRule<'i> { 35 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 36 | where 37 | W: std::fmt::Write, 38 | { 39 | #[cfg(feature = "sourcemap")] 40 | dest.add_mapping(self.loc); 41 | dest.write_char('@')?; 42 | self.vendor_prefix.to_css(dest)?; 43 | dest.write_str("viewport")?; 44 | self.declarations.to_css_block(dest) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/serialization.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | macro_rules! wrapper { 4 | ($name: ident, $value: ident $(, $t: ty)?) => { 5 | #[derive(serde::Serialize, serde::Deserialize)] 6 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 7 | pub struct $name { 8 | $value: T, 9 | } 10 | 11 | impl<'de, T> $name { 12 | pub fn serialize(value: &T, serializer: S) -> Result 13 | where 14 | S: serde::Serializer, 15 | T: serde::Serialize, 16 | { 17 | let wrapper = $name { $value: value }; 18 | serde::Serialize::serialize(&wrapper, serializer) 19 | } 20 | 21 | pub fn deserialize(deserializer: D) -> Result 22 | where 23 | D: serde::Deserializer<'de>, 24 | T: serde::Deserialize<'de>, 25 | { 26 | let v: $name = serde::Deserialize::deserialize(deserializer)?; 27 | Ok(v.$value) 28 | } 29 | } 30 | }; 31 | } 32 | 33 | wrapper!(ValueWrapper, value); 34 | wrapper!(PrefixWrapper, vendorPrefix, crate::vendor_prefix::VendorPrefix); 35 | -------------------------------------------------------------------------------- /src/values/alpha.rs: -------------------------------------------------------------------------------- 1 | //! CSS alpha values, used to represent opacity. 2 | 3 | use super::percentage::NumberOrPercentage; 4 | use crate::error::{ParserError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | #[cfg(feature = "visitor")] 8 | use crate::visitor::Visit; 9 | use cssparser::*; 10 | 11 | /// A CSS [``](https://www.w3.org/TR/css-color-4/#typedef-alpha-value), 12 | /// used to represent opacity. 13 | /// 14 | /// Parses either a `` or ``, but is always stored and serialized as a number. 15 | #[derive(Debug, Clone, PartialEq)] 16 | #[cfg_attr(feature = "visitor", derive(Visit))] 17 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] 18 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 19 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 20 | pub struct AlphaValue(pub f32); 21 | 22 | impl<'i> Parse<'i> for AlphaValue { 23 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 24 | match NumberOrPercentage::parse(input)? { 25 | NumberOrPercentage::Percentage(percent) => Ok(AlphaValue(percent.0)), 26 | NumberOrPercentage::Number(number) => Ok(AlphaValue(number)), 27 | } 28 | } 29 | } 30 | 31 | impl ToCss for AlphaValue { 32 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 33 | where 34 | W: std::fmt::Write, 35 | { 36 | self.0.to_css(dest) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/values/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common [CSS values](https://www.w3.org/TR/css3-values/) used across many properties. 2 | //! 3 | //! Each value provides parsing and serialization support using the [Parse](super::traits::Parse) 4 | //! and [ToCss](super::traits::ToCss) traits. In addition, many values support ways of manipulating 5 | //! them, including converting between representations and units, generating fallbacks for legacy 6 | //! browsers, minifying them, etc. 7 | //! 8 | //! # Example 9 | //! 10 | //! This example shows how you could parse a CSS color value, convert it to RGB, and re-serialize it. 11 | //! Similar patterns for parsing and serializing are possible across all value types. 12 | //! 13 | //! ``` 14 | //! use lightningcss::{ 15 | //! traits::{Parse, ToCss}, 16 | //! values::color::CssColor, 17 | //! printer::PrinterOptions 18 | //! }; 19 | //! 20 | //! let color = CssColor::parse_string("lch(50% 75 0)").unwrap(); 21 | //! let rgb = color.to_rgb().unwrap(); 22 | //! assert_eq!(rgb.to_css_string(PrinterOptions::default()).unwrap(), "#e1157b"); 23 | //! ``` 24 | //! 25 | //! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css` 26 | //! methods instead, rather than parsing from a string. 27 | 28 | #![deny(missing_docs)] 29 | 30 | pub mod alpha; 31 | pub mod angle; 32 | pub mod calc; 33 | pub mod color; 34 | pub mod easing; 35 | pub mod gradient; 36 | pub mod ident; 37 | pub mod image; 38 | pub mod length; 39 | pub mod number; 40 | pub mod percentage; 41 | pub mod position; 42 | pub mod ratio; 43 | pub mod rect; 44 | pub mod resolution; 45 | pub mod shape; 46 | pub mod size; 47 | pub mod string; 48 | pub mod syntax; 49 | pub mod time; 50 | pub mod url; 51 | -------------------------------------------------------------------------------- /src/values/number.rs: -------------------------------------------------------------------------------- 1 | //! CSS number values. 2 | 3 | use super::angle::impl_try_from_angle; 4 | use super::calc::Calc; 5 | use crate::error::{ParserError, PrinterError}; 6 | use crate::printer::Printer; 7 | use crate::traits::private::AddInternal; 8 | use crate::traits::{Map, Op, Parse, Sign, ToCss, Zero}; 9 | use cssparser::*; 10 | 11 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#numbers) value. 12 | /// 13 | /// Numbers may be explicit or computed by `calc()`, but are always stored and serialized 14 | /// as their computed value. 15 | pub type CSSNumber = f32; 16 | 17 | impl<'i> Parse<'i> for CSSNumber { 18 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 19 | match input.try_parse(Calc::parse) { 20 | Ok(Calc::Value(v)) => return Ok(*v), 21 | Ok(Calc::Number(n)) => return Ok(n), 22 | // Numbers are always compatible, so they will always compute to a value. 23 | Ok(_) => return Err(input.new_custom_error(ParserError::InvalidValue)), 24 | _ => {} 25 | } 26 | 27 | let number = input.expect_number()?; 28 | Ok(number) 29 | } 30 | } 31 | 32 | impl ToCss for CSSNumber { 33 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 34 | where 35 | W: std::fmt::Write, 36 | { 37 | let number = *self; 38 | if number != 0.0 && number.abs() < 1.0 { 39 | let mut s = String::new(); 40 | cssparser::ToCss::to_css(self, &mut s)?; 41 | if number < 0.0 { 42 | dest.write_char('-')?; 43 | dest.write_str(s.trim_start_matches("-").trim_start_matches("0")) 44 | } else { 45 | dest.write_str(s.trim_start_matches('0')) 46 | } 47 | } else { 48 | cssparser::ToCss::to_css(self, dest)?; 49 | Ok(()) 50 | } 51 | } 52 | } 53 | 54 | impl std::convert::Into> for CSSNumber { 55 | fn into(self) -> Calc { 56 | Calc::Value(Box::new(self)) 57 | } 58 | } 59 | 60 | impl std::convert::From> for CSSNumber { 61 | fn from(calc: Calc) -> CSSNumber { 62 | match calc { 63 | Calc::Value(v) => *v, 64 | Calc::Number(n) => n, 65 | _ => unreachable!(), 66 | } 67 | } 68 | } 69 | 70 | impl AddInternal for CSSNumber { 71 | fn add(self, other: Self) -> Self { 72 | self + other 73 | } 74 | } 75 | 76 | impl Op for CSSNumber { 77 | fn op f32>(&self, to: &Self, op: F) -> Self { 78 | op(*self, *to) 79 | } 80 | 81 | fn op_to T>(&self, rhs: &Self, op: F) -> T { 82 | op(*self, *rhs) 83 | } 84 | } 85 | 86 | impl Map for CSSNumber { 87 | fn map f32>(&self, op: F) -> Self { 88 | op(*self) 89 | } 90 | } 91 | 92 | impl Sign for CSSNumber { 93 | fn sign(&self) -> f32 { 94 | if *self == 0.0 { 95 | return if f32::is_sign_positive(*self) { 0.0 } else { -0.0 }; 96 | } 97 | self.signum() 98 | } 99 | } 100 | 101 | impl Zero for CSSNumber { 102 | fn zero() -> Self { 103 | 0.0 104 | } 105 | 106 | fn is_zero(&self) -> bool { 107 | *self == 0.0 108 | } 109 | } 110 | 111 | impl_try_from_angle!(CSSNumber); 112 | 113 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#integers) value. 114 | pub type CSSInteger = i32; 115 | 116 | impl<'i> Parse<'i> for CSSInteger { 117 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 118 | // TODO: calc?? 119 | let integer = input.expect_integer()?; 120 | Ok(integer) 121 | } 122 | } 123 | 124 | impl ToCss for CSSInteger { 125 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 126 | where 127 | W: std::fmt::Write, 128 | { 129 | cssparser::ToCss::to_css(self, dest)?; 130 | Ok(()) 131 | } 132 | } 133 | 134 | impl Zero for CSSInteger { 135 | fn zero() -> Self { 136 | 0 137 | } 138 | 139 | fn is_zero(&self) -> bool { 140 | *self == 0 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/values/ratio.rs: -------------------------------------------------------------------------------- 1 | //! CSS ratio values. 2 | 3 | use super::number::CSSNumber; 4 | use crate::error::{ParserError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | #[cfg(feature = "visitor")] 8 | use crate::visitor::Visit; 9 | use cssparser::*; 10 | 11 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#ratios) value, 12 | /// representing the ratio of two numeric values. 13 | #[derive(Debug, Clone, PartialEq)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "visitor", visit(visit_ratio, RATIOS))] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 18 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 19 | pub struct Ratio(pub CSSNumber, pub CSSNumber); 20 | 21 | impl<'i> Parse<'i> for Ratio { 22 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 23 | let first = CSSNumber::parse(input)?; 24 | let second = if input.try_parse(|input| input.expect_delim('/')).is_ok() { 25 | CSSNumber::parse(input)? 26 | } else { 27 | 1.0 28 | }; 29 | 30 | Ok(Ratio(first, second)) 31 | } 32 | } 33 | 34 | impl Ratio { 35 | /// Parses a ratio where both operands are required. 36 | pub fn parse_required<'i, 't>(input: &mut Parser<'i, 't>) -> Result>> { 37 | let first = CSSNumber::parse(input)?; 38 | input.expect_delim('/')?; 39 | let second = CSSNumber::parse(input)?; 40 | Ok(Ratio(first, second)) 41 | } 42 | } 43 | 44 | impl ToCss for Ratio { 45 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 46 | where 47 | W: std::fmt::Write, 48 | { 49 | self.0.to_css(dest)?; 50 | if self.1 != 1.0 { 51 | dest.delim('/', true)?; 52 | self.1.to_css(dest)?; 53 | } 54 | Ok(()) 55 | } 56 | } 57 | 58 | impl std::ops::Add for Ratio { 59 | type Output = Self; 60 | 61 | fn add(self, other: CSSNumber) -> Ratio { 62 | Ratio(self.0 + other, self.1) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/values/rect.rs: -------------------------------------------------------------------------------- 1 | //! Generic values for four sided properties. 2 | 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::traits::{IsCompatible, Parse, ToCss}; 6 | #[cfg(feature = "visitor")] 7 | use crate::visitor::Visit; 8 | use cssparser::*; 9 | 10 | /// A generic value that represents a value for four sides of a box, 11 | /// e.g. border-width, margin, padding, etc. 12 | /// 13 | /// When serialized, as few components as possible are written when 14 | /// there are duplicate values. 15 | #[derive(Clone, Debug, PartialEq, Eq)] 16 | #[cfg_attr(feature = "visitor", derive(Visit))] 17 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 18 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 19 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 20 | pub struct Rect( 21 | /// The top component. 22 | pub T, 23 | /// The right component. 24 | pub T, 25 | /// The bottom component. 26 | pub T, 27 | /// The left component. 28 | pub T, 29 | ); 30 | 31 | impl Rect { 32 | /// Returns a new `Rect` value. 33 | pub fn new(first: T, second: T, third: T, fourth: T) -> Self { 34 | Rect(first, second, third, fourth) 35 | } 36 | } 37 | 38 | impl Default for Rect { 39 | fn default() -> Rect { 40 | Rect::all(T::default()) 41 | } 42 | } 43 | 44 | impl Rect 45 | where 46 | T: Clone, 47 | { 48 | /// Returns a rect with all the values equal to `v`. 49 | pub fn all(v: T) -> Self { 50 | Rect::new(v.clone(), v.clone(), v.clone(), v) 51 | } 52 | 53 | /// Parses a new `Rect` value with the given parse function. 54 | pub fn parse_with<'i, 't, Parse>( 55 | input: &mut Parser<'i, 't>, 56 | parse: Parse, 57 | ) -> Result>> 58 | where 59 | Parse: Fn(&mut Parser<'i, 't>) -> Result>>, 60 | { 61 | let first = parse(input)?; 62 | let second = if let Ok(second) = input.try_parse(|i| parse(i)) { 63 | second 64 | } else { 65 | // 66 | return Ok(Self::new(first.clone(), first.clone(), first.clone(), first)); 67 | }; 68 | let third = if let Ok(third) = input.try_parse(|i| parse(i)) { 69 | third 70 | } else { 71 | // 72 | return Ok(Self::new(first.clone(), second.clone(), first, second)); 73 | }; 74 | let fourth = if let Ok(fourth) = input.try_parse(|i| parse(i)) { 75 | fourth 76 | } else { 77 | // 78 | return Ok(Self::new(first, second.clone(), third, second)); 79 | }; 80 | // 81 | Ok(Self::new(first, second, third, fourth)) 82 | } 83 | } 84 | 85 | impl<'i, T> Parse<'i> for Rect 86 | where 87 | T: Clone + PartialEq + Parse<'i>, 88 | { 89 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 90 | Self::parse_with(input, T::parse) 91 | } 92 | } 93 | 94 | impl ToCss for Rect 95 | where 96 | T: PartialEq + ToCss, 97 | { 98 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 99 | where 100 | W: std::fmt::Write, 101 | { 102 | self.0.to_css(dest)?; 103 | let same_vertical = self.0 == self.2; 104 | let same_horizontal = self.1 == self.3; 105 | if same_vertical && same_horizontal && self.0 == self.1 { 106 | return Ok(()); 107 | } 108 | dest.write_str(" ")?; 109 | self.1.to_css(dest)?; 110 | if same_vertical && same_horizontal { 111 | return Ok(()); 112 | } 113 | dest.write_str(" ")?; 114 | self.2.to_css(dest)?; 115 | if same_horizontal { 116 | return Ok(()); 117 | } 118 | dest.write_str(" ")?; 119 | self.3.to_css(dest) 120 | } 121 | } 122 | 123 | impl IsCompatible for Rect { 124 | fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { 125 | self.0.is_compatible(browsers) 126 | && self.1.is_compatible(browsers) 127 | && self.2.is_compatible(browsers) 128 | && self.3.is_compatible(browsers) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/values/resolution.rs: -------------------------------------------------------------------------------- 1 | //! CSS resolution values. 2 | 3 | use super::length::serialize_dimension; 4 | use super::number::CSSNumber; 5 | use crate::compat::Feature; 6 | use crate::error::{ParserError, PrinterError}; 7 | use crate::printer::Printer; 8 | use crate::traits::{Parse, ToCss}; 9 | #[cfg(feature = "visitor")] 10 | use crate::visitor::Visit; 11 | use cssparser::*; 12 | 13 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#resolution) value. 14 | #[derive(Debug, Clone, PartialEq)] 15 | #[cfg_attr(feature = "visitor", derive(Visit))] 16 | #[cfg_attr(feature = "visitor", visit(visit_resolution, RESOLUTIONS))] 17 | #[cfg_attr( 18 | feature = "serde", 19 | derive(serde::Serialize, serde::Deserialize), 20 | serde(tag = "type", content = "value", rename_all = "kebab-case") 21 | )] 22 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 23 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 24 | pub enum Resolution { 25 | /// A resolution in dots per inch. 26 | Dpi(CSSNumber), 27 | /// A resolution in dots per centimeter. 28 | Dpcm(CSSNumber), 29 | /// A resolution in dots per px. 30 | Dppx(CSSNumber), 31 | } 32 | 33 | impl<'i> Parse<'i> for Resolution { 34 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 35 | // TODO: calc? 36 | let location = input.current_source_location(); 37 | match *input.next()? { 38 | Token::Dimension { value, ref unit, .. } => { 39 | match_ignore_ascii_case! { unit, 40 | "dpi" => Ok(Resolution::Dpi(value)), 41 | "dpcm" => Ok(Resolution::Dpcm(value)), 42 | "dppx" | "x" => Ok(Resolution::Dppx(value)), 43 | _ => Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) 44 | } 45 | } 46 | ref t => Err(location.new_unexpected_token_error(t.clone())), 47 | } 48 | } 49 | } 50 | 51 | impl<'i> TryFrom<&Token<'i>> for Resolution { 52 | type Error = (); 53 | 54 | fn try_from(token: &Token) -> Result { 55 | match token { 56 | Token::Dimension { value, ref unit, .. } => match_ignore_ascii_case! { unit, 57 | "dpi" => Ok(Resolution::Dpi(*value)), 58 | "dpcm" => Ok(Resolution::Dpcm(*value)), 59 | "dppx" | "x" => Ok(Resolution::Dppx(*value)), 60 | _ => Err(()), 61 | }, 62 | _ => Err(()), 63 | } 64 | } 65 | } 66 | 67 | impl ToCss for Resolution { 68 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 69 | where 70 | W: std::fmt::Write, 71 | { 72 | let (value, unit) = match self { 73 | Resolution::Dpi(dpi) => (*dpi, "dpi"), 74 | Resolution::Dpcm(dpcm) => (*dpcm, "dpcm"), 75 | Resolution::Dppx(dppx) => { 76 | if dest.targets.current.is_compatible(Feature::XResolutionUnit) { 77 | (*dppx, "x") 78 | } else { 79 | (*dppx, "dppx") 80 | } 81 | } 82 | }; 83 | 84 | serialize_dimension(value, unit, dest) 85 | } 86 | } 87 | 88 | impl std::ops::Add for Resolution { 89 | type Output = Self; 90 | 91 | fn add(self, other: CSSNumber) -> Resolution { 92 | match self { 93 | Resolution::Dpi(dpi) => Resolution::Dpi(dpi + other), 94 | Resolution::Dpcm(dpcm) => Resolution::Dpcm(dpcm + other), 95 | Resolution::Dppx(dppx) => Resolution::Dppx(dppx + other), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/values/size.rs: -------------------------------------------------------------------------------- 1 | //! Generic values for two component properties. 2 | 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::traits::{IsCompatible, Parse, ToCss}; 6 | #[cfg(feature = "visitor")] 7 | use crate::visitor::Visit; 8 | use cssparser::*; 9 | 10 | /// A generic value that represents a value with two components, e.g. a border radius. 11 | /// 12 | /// When serialized, only a single component will be written if both are equal. 13 | #[derive(Debug, Clone, PartialEq)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 16 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 17 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 18 | pub struct Size2D(pub T, pub T); 19 | 20 | impl<'i, T> Parse<'i> for Size2D 21 | where 22 | T: Parse<'i> + Clone, 23 | { 24 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 25 | let first = T::parse(input)?; 26 | let second = input.try_parse(T::parse).unwrap_or_else(|_| first.clone()); 27 | Ok(Size2D(first, second)) 28 | } 29 | } 30 | 31 | impl ToCss for Size2D 32 | where 33 | T: ToCss + PartialEq, 34 | { 35 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 36 | where 37 | W: std::fmt::Write, 38 | { 39 | self.0.to_css(dest)?; 40 | if self.1 != self.0 { 41 | dest.write_str(" ")?; 42 | self.1.to_css(dest)?; 43 | } 44 | Ok(()) 45 | } 46 | } 47 | 48 | impl IsCompatible for Size2D { 49 | fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { 50 | self.0.is_compatible(browsers) && self.1.is_compatible(browsers) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/values/url.rs: -------------------------------------------------------------------------------- 1 | //! CSS url() values. 2 | 3 | use crate::dependencies::{Dependency, Location, UrlDependency}; 4 | use crate::error::{ParserError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | use crate::values::string::CowArcStr; 8 | #[cfg(feature = "visitor")] 9 | use crate::visitor::Visit; 10 | use cssparser::*; 11 | 12 | /// A CSS [url()](https://www.w3.org/TR/css-values-4/#urls) value and its source location. 13 | #[derive(Debug, Clone)] 14 | #[cfg_attr(feature = "visitor", derive(Visit))] 15 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 16 | #[cfg_attr(feature = "visitor", visit(visit_url, URLS))] 17 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 18 | #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] 19 | pub struct Url<'i> { 20 | /// The url string. 21 | #[cfg_attr(feature = "serde", serde(borrow))] 22 | pub url: CowArcStr<'i>, 23 | /// The location where the `url()` was seen in the CSS source file. 24 | pub loc: Location, 25 | } 26 | 27 | impl<'i> PartialEq for Url<'i> { 28 | fn eq(&self, other: &Self) -> bool { 29 | self.url == other.url 30 | } 31 | } 32 | 33 | impl<'i> Parse<'i> for Url<'i> { 34 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 35 | let loc = input.current_source_location(); 36 | let url = input.expect_url()?.into(); 37 | Ok(Url { url, loc: loc.into() }) 38 | } 39 | } 40 | 41 | impl<'i> ToCss for Url<'i> { 42 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 43 | where 44 | W: std::fmt::Write, 45 | { 46 | let dep = if dest.dependencies.is_some() { 47 | Some(UrlDependency::new(self, dest.filename())) 48 | } else { 49 | None 50 | }; 51 | 52 | // If adding dependencies, always write url() with quotes so that the placeholder can 53 | // be replaced without escaping more easily. Quotes may be removed later during minification. 54 | if let Some(dep) = dep { 55 | dest.write_str("url(")?; 56 | serialize_string(&dep.placeholder, dest)?; 57 | dest.write_char(')')?; 58 | 59 | if let Some(dependencies) = &mut dest.dependencies { 60 | dependencies.push(Dependency::Url(dep)) 61 | } 62 | 63 | return Ok(()); 64 | } 65 | 66 | use cssparser::ToCss; 67 | if dest.minify { 68 | let mut buf = String::new(); 69 | Token::UnquotedUrl(CowRcStr::from(self.url.as_ref())).to_css(&mut buf)?; 70 | 71 | // If the unquoted url is longer than it would be quoted (e.g. `url("...")`) 72 | // then serialize as a string and choose the shorter version. 73 | if buf.len() > self.url.len() + 7 { 74 | let mut buf2 = String::new(); 75 | serialize_string(&self.url, &mut buf2)?; 76 | if buf2.len() + 5 < buf.len() { 77 | dest.write_str("url(")?; 78 | dest.write_str(&buf2)?; 79 | return dest.write_char(')'); 80 | } 81 | } 82 | 83 | dest.write_str(&buf)?; 84 | } else { 85 | dest.write_str("url(")?; 86 | serialize_string(&self.url, dest)?; 87 | dest.write_char(')')?; 88 | } 89 | 90 | Ok(()) 91 | } 92 | } 93 | 94 | impl<'i> Url<'i> { 95 | /// Returns whether the URL is absolute, and not relative. 96 | pub fn is_absolute(&self) -> bool { 97 | let url = self.url.as_ref(); 98 | 99 | // Quick checks. If the url starts with '.', it is relative. 100 | if url.starts_with('.') { 101 | return false; 102 | } 103 | 104 | // If the url starts with '/' it is absolute. 105 | if url.starts_with('/') { 106 | return true; 107 | } 108 | 109 | // If the url starts with '#' we have a fragment URL. 110 | // These are resolved relative to the document rather than the CSS file. 111 | // https://drafts.csswg.org/css-values-4/#local-urls 112 | if url.starts_with('#') { 113 | return true; 114 | } 115 | 116 | // Otherwise, we might have a scheme. These must start with an ascii alpha character. 117 | // https://url.spec.whatwg.org/#scheme-start-state 118 | if !url.starts_with(|c| matches!(c, 'a'..='z' | 'A'..='Z')) { 119 | return false; 120 | } 121 | 122 | // https://url.spec.whatwg.org/#scheme-state 123 | for b in url.as_bytes() { 124 | let c = *b as char; 125 | match c { 126 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.' => {} 127 | ':' => return true, 128 | _ => break, 129 | } 130 | } 131 | 132 | false 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/vendor_prefix.rs: -------------------------------------------------------------------------------- 1 | //! Vendor prefixes. 2 | 3 | #![allow(non_upper_case_globals)] 4 | 5 | use crate::error::PrinterError; 6 | use crate::printer::Printer; 7 | use crate::traits::ToCss; 8 | #[cfg(feature = "visitor")] 9 | use crate::visitor::{Visit, VisitTypes, Visitor}; 10 | use bitflags::bitflags; 11 | 12 | bitflags! { 13 | /// Bit flags that represent one or more vendor prefixes, such as 14 | /// `-webkit` or `-moz`. 15 | /// 16 | /// Multiple flags can be combined to represent 17 | /// more than one prefix. During printing, the rule or property will 18 | /// be duplicated for each prefix flag that is enabled. This enables 19 | /// vendor prefixes to be added without increasing memory usage. 20 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] 21 | #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] 22 | pub struct VendorPrefix: u8 { 23 | /// The `-webkit` vendor prefix. 24 | const WebKit = 0b00000010; 25 | /// The `-moz` vendor prefix. 26 | const Moz = 0b00000100; 27 | /// The `-ms` vendor prefix. 28 | const Ms = 0b00001000; 29 | /// The `-o` vendor prefix. 30 | const O = 0b00010000; 31 | /// No vendor prefixes. 32 | const None = 0b00000001; 33 | } 34 | } 35 | 36 | impl Default for VendorPrefix { 37 | fn default() -> VendorPrefix { 38 | VendorPrefix::None 39 | } 40 | } 41 | 42 | impl VendorPrefix { 43 | /// Returns a vendor prefix flag from a prefix string (without the leading `-`). 44 | pub fn from_str(s: &str) -> VendorPrefix { 45 | match s { 46 | "webkit" => VendorPrefix::WebKit, 47 | "moz" => VendorPrefix::Moz, 48 | "ms" => VendorPrefix::Ms, 49 | "o" => VendorPrefix::O, 50 | _ => unreachable!(), 51 | } 52 | } 53 | 54 | /// Returns VendorPrefix::None if empty. 55 | #[inline] 56 | pub fn or_none(self) -> Self { 57 | self.or(VendorPrefix::None) 58 | } 59 | 60 | /// Returns `other` if `self` is empty 61 | #[inline] 62 | pub fn or(self, other: Self) -> Self { 63 | if self.is_empty() { 64 | other 65 | } else { 66 | self 67 | } 68 | } 69 | } 70 | 71 | impl ToCss for VendorPrefix { 72 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 73 | where 74 | W: std::fmt::Write, 75 | { 76 | cssparser::ToCss::to_css(self, dest)?; 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl cssparser::ToCss for VendorPrefix { 82 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 83 | where 84 | W: std::fmt::Write, 85 | { 86 | match *self { 87 | VendorPrefix::WebKit => dest.write_str("-webkit-"), 88 | VendorPrefix::Moz => dest.write_str("-moz-"), 89 | VendorPrefix::Ms => dest.write_str("-ms-"), 90 | VendorPrefix::O => dest.write_str("-o-"), 91 | _ => Ok(()), 92 | } 93 | } 94 | } 95 | 96 | #[cfg(feature = "serde")] 97 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 98 | impl serde::Serialize for VendorPrefix { 99 | fn serialize(&self, serializer: S) -> Result 100 | where 101 | S: serde::Serializer, 102 | { 103 | let mut values = Vec::new(); 104 | if *self != VendorPrefix::None { 105 | if self.contains(VendorPrefix::None) { 106 | values.push("none"); 107 | } 108 | if self.contains(VendorPrefix::WebKit) { 109 | values.push("webkit"); 110 | } 111 | if self.contains(VendorPrefix::Moz) { 112 | values.push("moz"); 113 | } 114 | if self.contains(VendorPrefix::Ms) { 115 | values.push("ms"); 116 | } 117 | if self.contains(VendorPrefix::O) { 118 | values.push("o"); 119 | } 120 | } 121 | values.serialize(serializer) 122 | } 123 | } 124 | 125 | #[cfg(feature = "serde")] 126 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 127 | impl<'de> serde::Deserialize<'de> for VendorPrefix { 128 | fn deserialize(deserializer: D) -> Result 129 | where 130 | D: serde::Deserializer<'de>, 131 | { 132 | use crate::values::string::CowArcStr; 133 | let values = Vec::>::deserialize(deserializer)?; 134 | if values.is_empty() { 135 | return Ok(VendorPrefix::None); 136 | } 137 | let mut res = VendorPrefix::empty(); 138 | for value in values { 139 | res |= match value.as_ref() { 140 | "none" => VendorPrefix::None, 141 | "webkit" => VendorPrefix::WebKit, 142 | "moz" => VendorPrefix::Moz, 143 | "ms" => VendorPrefix::Ms, 144 | "o" => VendorPrefix::O, 145 | _ => continue, 146 | }; 147 | } 148 | Ok(res) 149 | } 150 | } 151 | 152 | #[cfg(feature = "visitor")] 153 | #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] 154 | impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for VendorPrefix { 155 | const CHILD_TYPES: VisitTypes = VisitTypes::empty(); 156 | fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> { 157 | Ok(()) 158 | } 159 | } 160 | 161 | #[cfg(feature = "jsonschema")] 162 | #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] 163 | impl schemars::JsonSchema for VendorPrefix { 164 | fn is_referenceable() -> bool { 165 | true 166 | } 167 | 168 | fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { 169 | #[derive(schemars::JsonSchema)] 170 | #[schemars(rename_all = "lowercase")] 171 | #[allow(dead_code)] 172 | enum Prefix { 173 | None, 174 | WebKit, 175 | Moz, 176 | Ms, 177 | O, 178 | } 179 | 180 | Vec::::json_schema(gen) 181 | } 182 | 183 | fn schema_name() -> String { 184 | "VendorPrefix".into() 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /static-self-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-self-derive" 3 | version = "0.1.1" 4 | edition = "2021" 5 | authors = ["Devon Govett ","Donny "] 6 | description = "Derive macros for static-self" 7 | license = "MPL-2.0" 8 | keywords = [ ] 9 | repository = "https://github.com/parcel-bundler/lightningcss" 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "1.0", features = ["extra-traits"] } 17 | quote = "1.0" 18 | proc-macro2 = "1.0" 19 | -------------------------------------------------------------------------------- /static-self-derive/src/into_owned.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{self, TokenStream}; 2 | use proc_macro2::Span; 3 | use quote::quote; 4 | use syn::{parse_macro_input, parse_quote, Data, DataEnum, DeriveInput, Field, Fields, Ident, Member}; 5 | 6 | pub(crate) fn derive_into_owned(input: TokenStream) -> TokenStream { 7 | let DeriveInput { 8 | ident: self_name, 9 | data, 10 | mut generics, 11 | .. 12 | } = parse_macro_input!(input); 13 | 14 | let res = match data { 15 | Data::Struct(s) => { 16 | let fields = s 17 | .fields 18 | .iter() 19 | .enumerate() 20 | .map(|(index, Field { ident, .. })| { 21 | let name = ident 22 | .as_ref() 23 | .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone())); 24 | 25 | let value = into_owned(quote! { self.#name }); 26 | if let Some(ident) = ident { 27 | quote! { #ident: #value } 28 | } else { 29 | value 30 | } 31 | }) 32 | .collect::>(); 33 | 34 | match s.fields { 35 | Fields::Unnamed(_) => { 36 | quote! { 37 | #self_name(#(#fields),*) 38 | } 39 | } 40 | Fields::Named(_) => { 41 | quote! { 42 | #self_name { #(#fields),* } 43 | } 44 | } 45 | Fields::Unit => quote! {}, 46 | } 47 | } 48 | Data::Enum(DataEnum { variants, .. }) => { 49 | let variants = variants 50 | .iter() 51 | .map(|variant| { 52 | let name = &variant.ident; 53 | let mut field_names = Vec::new(); 54 | let mut static_fields = Vec::new(); 55 | for (index, Field { ident, .. }) in variant.fields.iter().enumerate() { 56 | let name = ident.as_ref().map_or_else( 57 | || Ident::new(&format!("_{}", index), Span::call_site()), 58 | |ident| ident.clone(), 59 | ); 60 | field_names.push(name.clone()); 61 | let value = into_owned(quote! { #name }); 62 | static_fields.push(if let Some(ident) = ident { 63 | quote! { #ident: #value } 64 | } else { 65 | value 66 | }) 67 | } 68 | 69 | match variant.fields { 70 | Fields::Unnamed(_) => { 71 | quote! { 72 | #self_name::#name(#(#field_names),*) => { 73 | #self_name::#name(#(#static_fields),*) 74 | } 75 | } 76 | } 77 | Fields::Named(_) => { 78 | quote! { 79 | #self_name::#name { #(#field_names),* } => { 80 | #self_name::#name { #(#static_fields),* } 81 | } 82 | } 83 | } 84 | Fields::Unit => quote! { 85 | #self_name::#name => #self_name::#name, 86 | }, 87 | } 88 | }) 89 | .collect::(); 90 | 91 | quote! { 92 | match self { 93 | #variants 94 | } 95 | } 96 | } 97 | _ => { 98 | panic!("can only derive IntoOwned for enums and structs") 99 | } 100 | }; 101 | 102 | let orig_generics = generics.clone(); 103 | 104 | // Add generic bounds for all type parameters. 105 | let mut type_param_names = vec![]; 106 | 107 | for ty in generics.type_params() { 108 | type_param_names.push(ty.ident.clone()); 109 | } 110 | 111 | for type_param in type_param_names { 112 | generics.make_where_clause().predicates.push_value(parse_quote! { 113 | #type_param: ::static_self::IntoOwned<'any> 114 | }) 115 | } 116 | 117 | let has_lifetime = generics 118 | .params 119 | .first() 120 | .map_or(false, |v| matches!(v, syn::GenericParam::Lifetime(..))); 121 | let has_generic = !generics.params.is_empty(); 122 | 123 | // Prepend `'any` to generics 124 | let any = syn::GenericParam::Lifetime(syn::LifetimeDef { 125 | attrs: Default::default(), 126 | lifetime: syn::Lifetime { 127 | apostrophe: Span::call_site(), 128 | ident: Ident::new("any", Span::call_site()), 129 | }, 130 | colon_token: None, 131 | bounds: Default::default(), 132 | }); 133 | generics.params.insert(0, any.clone()); 134 | 135 | let (impl_generics, _, where_clause) = generics.split_for_impl(); 136 | let (_, ty_generics, _) = orig_generics.split_for_impl(); 137 | 138 | let into_owned = if !has_generic { 139 | quote! { 140 | impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause { 141 | type Owned = Self; 142 | 143 | #[inline] 144 | fn into_owned(self) -> Self { 145 | self 146 | } 147 | } 148 | } 149 | } else { 150 | let mut generics_without_default = generics.clone(); 151 | 152 | let mut params = Vec::new(); 153 | 154 | for p in generics_without_default.params.iter_mut() { 155 | if let syn::GenericParam::Type(ty) = p { 156 | ty.default = None; 157 | 158 | params.push(quote!(<#ty as static_self::IntoOwned<'any>>::Owned)); 159 | } 160 | } 161 | 162 | if has_lifetime { 163 | quote! { 164 | impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause { 165 | type Owned = #self_name<'any, #(#params),*>; 166 | /// Consumes the value and returns an owned clone. 167 | fn into_owned(self) -> Self::Owned { 168 | use ::static_self::IntoOwned; 169 | 170 | #res 171 | } 172 | } 173 | } 174 | } else { 175 | quote! { 176 | impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause { 177 | type Owned = #self_name<#(#params),*>; 178 | /// Consumes the value and returns an owned clone. 179 | fn into_owned(self) -> Self::Owned { 180 | use ::static_self::IntoOwned; 181 | 182 | #res 183 | } 184 | } 185 | } 186 | } 187 | }; 188 | 189 | into_owned.into() 190 | } 191 | 192 | fn into_owned(name: proc_macro2::TokenStream) -> proc_macro2::TokenStream { 193 | quote! { #name.into_owned() } 194 | } 195 | -------------------------------------------------------------------------------- /static-self-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod into_owned; 4 | 5 | #[proc_macro_derive(IntoOwned)] 6 | pub fn derive_into_owned(input: TokenStream) -> TokenStream { 7 | into_owned::derive_into_owned(input) 8 | } 9 | -------------------------------------------------------------------------------- /static-self/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-self" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Devon Govett ","Donny "] 6 | description = "A trait for values that can be cloned with a static lifetime" 7 | license = "MPL-2.0" 8 | keywords = [ ] 9 | repository = "https://github.com/parcel-bundler/lightningcss" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | smallvec = { version = "1.11.1", optional = true } 15 | static-self-derive = { version = "0.1.1", path = "../static-self-derive" } 16 | indexmap = { version = "2.2.6", optional = true } 17 | -------------------------------------------------------------------------------- /static-self/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use static_self_derive::IntoOwned; 2 | 3 | /// A trait for things that can be cloned with a new lifetime. 4 | /// 5 | /// `'any` lifeitme means the output should have `'static` lifetime. 6 | pub trait IntoOwned<'any> { 7 | /// A variant of `Self` with a new lifetime. 8 | type Owned: 'any; 9 | 10 | /// Make lifetime of `self` `'static`. 11 | fn into_owned(self) -> Self::Owned; 12 | } 13 | 14 | macro_rules! impl_into_owned { 15 | ($t: ty) => { 16 | impl<'a> IntoOwned<'a> for $t { 17 | type Owned = Self; 18 | 19 | #[inline] 20 | fn into_owned(self) -> Self { 21 | self 22 | } 23 | } 24 | }; 25 | ($($t:ty),*) => { 26 | $(impl_into_owned!($t);)* 27 | }; 28 | } 29 | 30 | impl_into_owned!(bool, f32, f64, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize, char, String); 31 | 32 | macro_rules! impl_tuple { 33 | ( 34 | $($name:ident),* 35 | ) =>{ 36 | #[allow(non_snake_case)] 37 | impl<'any, $($name,)*> IntoOwned<'any> for ($($name,)*) 38 | where 39 | $($name: IntoOwned<'any>),* 40 | { 41 | type Owned = ($(<$name as IntoOwned<'any>>::Owned,)*); 42 | 43 | #[inline] 44 | fn into_owned(self) -> Self::Owned { 45 | let ($($name,)*) = self; 46 | ($($name.into_owned(),)*) 47 | } 48 | } 49 | }; 50 | } 51 | 52 | macro_rules! call_impl_tuple { 53 | () => {}; 54 | ($first:ident) => { 55 | impl_tuple!($first); 56 | }; 57 | ( 58 | $first:ident, 59 | $($name:ident),* 60 | ) => { 61 | call_impl_tuple!($($name),*); 62 | impl_tuple!($first, $($name),*); 63 | }; 64 | } 65 | 66 | call_impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L); 67 | 68 | impl<'any, T> IntoOwned<'any> for Vec 69 | where 70 | T: IntoOwned<'any>, 71 | { 72 | type Owned = Vec<>::Owned>; 73 | 74 | fn into_owned(self) -> Self::Owned { 75 | self.into_iter().map(|v| v.into_owned()).collect() 76 | } 77 | } 78 | impl<'any, T> IntoOwned<'any> for Option 79 | where 80 | T: IntoOwned<'any>, 81 | { 82 | type Owned = Option<>::Owned>; 83 | 84 | fn into_owned(self) -> Self::Owned { 85 | self.map(|v| v.into_owned()) 86 | } 87 | } 88 | 89 | impl<'any, T> IntoOwned<'any> for Box 90 | where 91 | T: IntoOwned<'any>, 92 | { 93 | type Owned = Box<>::Owned>; 94 | 95 | fn into_owned(self) -> Self::Owned { 96 | Box::new((*self).into_owned()) 97 | } 98 | } 99 | 100 | impl<'any, T> IntoOwned<'any> for Box<[T]> 101 | where 102 | T: IntoOwned<'any>, 103 | { 104 | type Owned = Box<[>::Owned]>; 105 | 106 | fn into_owned(self) -> Self::Owned { 107 | self.into_vec().into_owned().into_boxed_slice() 108 | } 109 | } 110 | 111 | #[cfg(feature = "smallvec")] 112 | impl<'any, T, const N: usize> IntoOwned<'any> for smallvec::SmallVec<[T; N]> 113 | where 114 | T: IntoOwned<'any>, 115 | [T; N]: smallvec::Array, 116 | [>::Owned; N]: smallvec::Array>::Owned>, 117 | { 118 | type Owned = smallvec::SmallVec<[>::Owned; N]>; 119 | 120 | fn into_owned(self) -> Self::Owned { 121 | self.into_iter().map(|v| v.into_owned()).collect() 122 | } 123 | } 124 | 125 | #[cfg(feature = "indexmap")] 126 | impl<'any, K, V> IntoOwned<'any> for indexmap::IndexMap 127 | where 128 | K: IntoOwned<'any>, 129 | V: IntoOwned<'any>, 130 | >::Owned: Eq + std::hash::Hash, 131 | { 132 | type Owned = indexmap::IndexMap<>::Owned, >::Owned>; 133 | 134 | fn into_owned(self) -> Self::Owned { 135 | self.into_iter().map(|(k, v)| (k.into_owned(), v.into_owned())).collect() 136 | } 137 | } 138 | 139 | impl<'any, T, const N: usize> IntoOwned<'any> for [T; N] 140 | where 141 | T: IntoOwned<'any>, 142 | { 143 | type Owned = [>::Owned; N]; 144 | 145 | fn into_owned(self) -> Self::Owned { 146 | self 147 | .into_iter() 148 | .map(|v| v.into_owned()) 149 | .collect::>() 150 | .try_into() 151 | .unwrap_or_else(|_| unreachable!("Vec with N elements should be able to be converted to [T; N]")) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test-integration.mjs: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import fetch from 'node-fetch'; 3 | import assert from 'assert'; 4 | import {diff} from 'jest-diff'; 5 | import * as css from 'lightningcss'; 6 | 7 | let urls = [ 8 | 'https://getbootstrap.com/docs/5.1/examples/headers/', 9 | 'https://getbootstrap.com/docs/5.1/examples/heroes/', 10 | 'https://getbootstrap.com/docs/5.1/examples/features/', 11 | 'https://getbootstrap.com/docs/5.1/examples/sidebars/', 12 | 'https://getbootstrap.com/docs/5.1/examples/footers/', 13 | 'https://getbootstrap.com/docs/5.1/examples/dropdowns/', 14 | 'https://getbootstrap.com/docs/5.1/examples/list-groups/', 15 | 'https://getbootstrap.com/docs/5.1/examples/modals/', 16 | 'http://csszengarden.com', 17 | 'http://csszengarden.com/221/', 18 | 'http://csszengarden.com/219/', 19 | 'http://csszengarden.com/218/', 20 | 'http://csszengarden.com/217/', 21 | 'http://csszengarden.com/216/', 22 | 'http://csszengarden.com/215/' 23 | ]; 24 | 25 | let success = true; 26 | const browser = await puppeteer.launch(); 27 | const page = await browser.newPage(); 28 | 29 | for (let url of urls) { 30 | await test(url); 31 | } 32 | 33 | async function test(url) { 34 | console.log(`Testing ${url}...`); 35 | 36 | await page.goto(url); 37 | await page.waitForNetworkIdle(); 38 | 39 | // Snapshot the computed styles of all elements 40 | let elements = await page.$$('body *'); 41 | let computed = []; 42 | for (let element of elements) { 43 | let style = await element.evaluate(node => { 44 | let res = {}; 45 | let style = window.getComputedStyle(node); 46 | for (let i = 0; i < style.length; i++) { 47 | res[style.item(i)] = style.getPropertyValue(style.item(i)); 48 | } 49 | return res; 50 | }); 51 | 52 | for (let key in style) { 53 | style[key] = normalize(key, style[key]); 54 | } 55 | 56 | computed.push(style); 57 | } 58 | 59 | // Find stylesheets, load, and minify. 60 | let stylesheets = await page.evaluate(() => { 61 | return [...document.styleSheets].map(styleSheet => styleSheet.href).filter(Boolean); 62 | }); 63 | 64 | let texts = await Promise.all(stylesheets.map(async url => { 65 | let res = await fetch(url); 66 | return res.text(); 67 | })); 68 | 69 | let minified = texts.map((code, i) => { 70 | let minified = css.transform({ 71 | filename: 'test.css', 72 | code: Buffer.from(code), 73 | minify: true, 74 | targets: { 75 | chrome: 95 << 16 76 | } 77 | }).code.toString(); 78 | console.log(new URL(stylesheets[i]).pathname, '–', code.length + ' bytes', '=>', minified.length + ' bytes'); 79 | return minified; 80 | }); 81 | 82 | await page.setCacheEnabled(false); 83 | 84 | // Disable the original stylesheets and insert a