├── rustfmt.toml ├── cli ├── .gitignore ├── parcel_css └── postinstall.js ├── rust-toolchain.toml ├── .gitignore ├── .prettierrc ├── node ├── build.rs ├── targets.d.ts ├── index.js ├── Cargo.toml ├── browserslistToTargets.js └── index.d.ts ├── src ├── logical.rs ├── targets.rs ├── rules │ ├── custom_media.rs │ ├── viewport.rs │ ├── counter_style.rs │ ├── namespace.rs │ ├── document.rs │ ├── nesting.rs │ ├── import.rs │ ├── media.rs │ ├── page.rs │ ├── layer.rs │ ├── property.rs │ ├── style.rs │ └── supports.rs ├── values │ ├── alpha.rs │ ├── size.rs │ ├── mod.rs │ ├── ratio.rs │ ├── resolution.rs │ ├── ident.rs │ ├── number.rs │ ├── rect.rs │ ├── url.rs │ ├── time.rs │ ├── angle.rs │ ├── string.rs │ └── easing.rs ├── vendor_prefix.rs ├── properties │ ├── outline.rs │ ├── css_modules.rs │ ├── position.rs │ ├── overflow.rs │ ├── prefix_handler.rs │ ├── box_shadow.rs │ └── size.rs ├── traits.rs ├── dependencies.rs ├── css_modules.rs ├── printer.rs ├── context.rs ├── stylesheet.rs └── main.rs ├── .cargo └── config.toml ├── .github └── workflows │ └── test.yml ├── scripts ├── build-flow.js ├── build.js ├── build-wasm.js └── build-npm.js ├── selectors ├── Cargo.toml ├── lib.rs ├── sink.rs ├── README.md ├── build.rs ├── nth_index_cache.rs ├── visitor.rs ├── tree.rs └── attr.rs ├── bench.js ├── test.js ├── Cargo.toml ├── package.json ├── playground ├── index.html └── playground.js └── test-integration.mjs /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | chain_width = 80 3 | max_width = 115 -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | package.json 2 | README.md 3 | .DS_Store 4 | parcel_css.exe 5 | -------------------------------------------------------------------------------- /cli/parcel_css: -------------------------------------------------------------------------------- 1 | This file is required so that npm creates the parcel-css binary on Windows. 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.60.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.node 2 | node_modules/ 3 | target/ 4 | pkg/ 5 | dist/ 6 | .parcel-cache 7 | node/*.flow 8 | artifacts 9 | npm 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "endOfLine": "lf", 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/logical.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub(crate) enum PropertyCategory { 3 | Logical, 4 | Physical, 5 | } 6 | 7 | impl Default for PropertyCategory { 8 | fn default() -> PropertyCategory { 9 | PropertyCategory::Physical 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | [target.aarch64-unknown-linux-gnu] 3 | linker = "aarch64-linux-gnu-gcc" 4 | 5 | [target.armv7-unknown-linux-gnueabihf] 6 | linker = "arm-linux-gnueabihf-gcc" 7 | 8 | [target.aarch64-unknown-linux-musl] 9 | linker = "aarch64-linux-musl-gcc" 10 | rustflags = ["-C", "target-feature=-crt-static"] 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/targets.rs: -------------------------------------------------------------------------------- 1 | // This file is autogenerated by build-prefixes.js. DO NOT EDIT! 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Debug, Deserialize, Clone, Copy, Default)] 6 | pub struct Browsers { 7 | pub android: Option, 8 | pub chrome: Option, 9 | pub edge: Option, 10 | pub firefox: Option, 11 | pub ie: Option, 12 | pub ios_saf: Option, 13 | pub opera: Option, 14 | pub safari: Option, 15 | pub samsung: Option, 16 | } 17 | -------------------------------------------------------------------------------- /.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@v2 18 | - uses: dtolnay/rust-toolchain@stable 19 | - uses: Swatinem/rust-cache@v1 20 | - run: cargo fmt 21 | - run: cargo test 22 | -------------------------------------------------------------------------------- /scripts/build-flow.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | let dir = `${__dirname}/../`; 4 | let index = fs.readFileSync(dir + '/node/index.d.ts', 'utf8'); 5 | index = '// @flow\n' + index; 6 | index = index.replace(/export interface (.*?) \{((?:.|\n)*?)\}/g, 'export type $1 = {|$2|};'); 7 | index = index.replace(/export declare function/g, 'declare export function'); 8 | 9 | let targets = fs.readFileSync(dir + '/node/targets.d.ts', 'utf8'); 10 | targets = targets.replace(/export interface (.*?) \{((?:.|\n)*?)\}/g, 'export type $1 = {|$2|};'); 11 | index = index.replace("import type {Targets} from './targets';", targets); 12 | 13 | fs.writeFileSync(dir + '/node/index.js.flow', index); 14 | -------------------------------------------------------------------------------- /selectors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parcel_selectors" 3 | version = "0.24.4" 4 | authors = ["The Servo Project Developers"] 5 | documentation = "https://docs.rs/selectors/" 6 | description = "CSS Selectors matching for Rust - forked for parcel_css" 7 | repository = "https://github.com/servo/servo" 8 | readme = "README.md" 9 | keywords = ["css", "selectors"] 10 | license = "MPL-2.0" 11 | build = "build.rs" 12 | 13 | [lib] 14 | name = "parcel_selectors" 15 | path = "lib.rs" 16 | 17 | [features] 18 | bench = [] 19 | 20 | [dependencies] 21 | bitflags = "1.0" 22 | cssparser = "0.29" 23 | derive_more = "0.99" 24 | fxhash = "0.2" 25 | log = "0.4" 26 | phf = "0.8" 27 | precomputed-hash = "0.1" 28 | smallvec = "1.0" 29 | 30 | [build-dependencies] 31 | phf_codegen = "0.8" 32 | -------------------------------------------------------------------------------- /src/rules/custom_media.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use crate::error::PrinterError; 3 | use crate::media_query::MediaList; 4 | use crate::printer::Printer; 5 | use crate::traits::ToCss; 6 | use crate::values::ident::DashedIdent; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct CustomMediaRule<'i> { 10 | pub name: DashedIdent<'i>, 11 | pub query: MediaList<'i>, 12 | pub loc: Location, 13 | } 14 | 15 | impl<'i> ToCss for CustomMediaRule<'i> { 16 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 17 | where 18 | W: std::fmt::Write, 19 | { 20 | dest.add_mapping(self.loc); 21 | dest.write_str("@custom-media ")?; 22 | self.name.to_css(dest)?; 23 | dest.write_char(' ')?; 24 | self.query.to_css(dest) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | let parts = [process.platform, process.arch]; 2 | if (process.platform === 'linux') { 3 | const {MUSL, family} = require('detect-libc'); 4 | if (family === MUSL) { 5 | parts.push('musl'); 6 | } else if (process.arch === 'arm') { 7 | parts.push('gnueabihf'); 8 | } else { 9 | parts.push('gnu'); 10 | } 11 | } else if (process.platform === 'win32') { 12 | parts.push('msvc'); 13 | } 14 | 15 | if (process.env.CSS_TRANSFORMER_WASM) { 16 | module.exports = require(`../pkg`); 17 | } else { 18 | try { 19 | module.exports = require(`@parcel/css-${parts.join('-')}`); 20 | } catch (err) { 21 | module.exports = require(`../parcel-css.${parts.join('-')}.node`); 22 | } 23 | } 24 | 25 | module.exports.browserslistToTargets = require('./browserslistToTargets'); 26 | -------------------------------------------------------------------------------- /src/rules/viewport.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use crate::declaration::DeclarationBlock; 3 | use crate::error::PrinterError; 4 | use crate::printer::Printer; 5 | use crate::traits::ToCss; 6 | use crate::vendor_prefix::VendorPrefix; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct ViewportRule<'i> { 10 | pub vendor_prefix: VendorPrefix, 11 | pub declarations: DeclarationBlock<'i>, 12 | pub loc: Location, 13 | } 14 | 15 | impl<'i> ToCss for ViewportRule<'i> { 16 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 17 | where 18 | W: std::fmt::Write, 19 | { 20 | dest.add_mapping(self.loc); 21 | dest.write_char('@')?; 22 | self.vendor_prefix.to_css(dest)?; 23 | dest.write_str("viewport")?; 24 | self.declarations.to_css(dest) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rules/counter_style.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use crate::declaration::DeclarationBlock; 3 | use crate::error::PrinterError; 4 | use crate::printer::Printer; 5 | use crate::traits::ToCss; 6 | use crate::values::ident::CustomIdent; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct CounterStyleRule<'i> { 10 | pub name: CustomIdent<'i>, 11 | // TODO: eventually parse these properties 12 | pub declarations: DeclarationBlock<'i>, 13 | pub loc: Location, 14 | } 15 | 16 | impl<'i> ToCss for CounterStyleRule<'i> { 17 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 18 | where 19 | W: std::fmt::Write, 20 | { 21 | dest.add_mapping(self.loc); 22 | dest.write_str("@counter-style ")?; 23 | self.name.to_css(dest)?; 24 | self.declarations.to_css(dest) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rules/namespace.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use crate::error::PrinterError; 3 | use crate::printer::Printer; 4 | use crate::traits::ToCss; 5 | use crate::values::string::CowArcStr; 6 | use cssparser::*; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct NamespaceRule<'i> { 10 | pub prefix: Option>, 11 | pub url: CowArcStr<'i>, 12 | pub loc: Location, 13 | } 14 | 15 | impl<'i> ToCss for NamespaceRule<'i> { 16 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 17 | where 18 | W: std::fmt::Write, 19 | { 20 | dest.add_mapping(self.loc); 21 | dest.write_str("@namespace ")?; 22 | if let Some(prefix) = &self.prefix { 23 | serialize_identifier(&prefix, dest)?; 24 | dest.write_char(' ')?; 25 | } 26 | 27 | serialize_string(&self.url, dest)?; 28 | dest.write_char(';') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Devon Govett "] 3 | name = "parcel_css_node" 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | serde = { version = "1.0.123", features = ["derive"] } 13 | serde_bytes = "0.11.5" 14 | serde_json = "*" 15 | cssparser = "0.29.1" 16 | parcel_css = { path = "../" } 17 | parcel_sourcemap = "2.0.2" 18 | 19 | [target.'cfg(target_os = "macos")'.dependencies] 20 | jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } 21 | 22 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 23 | napi = { version = "1.7.10", features = ["serde-json"] } 24 | napi-derive = "1" 25 | 26 | [target.'cfg(target_arch = "wasm32")'.dependencies] 27 | js-sys = "0.3" 28 | serde-wasm-bindgen = "0.3.0" 29 | wasm-bindgen = "0.2" 30 | 31 | [target.'cfg(not(target_arch = "wasm32"))'.build-dependencies] 32 | napi-build = "1" 33 | -------------------------------------------------------------------------------- /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 derive_more; 14 | extern crate fxhash; 15 | #[macro_use] 16 | extern crate log; 17 | extern crate phf; 18 | extern crate precomputed_hash; 19 | extern crate smallvec; 20 | 21 | pub mod attr; 22 | pub mod bloom; 23 | mod builder; 24 | pub mod context; 25 | pub mod matching; 26 | mod nth_index_cache; 27 | pub mod parser; 28 | pub mod sink; 29 | mod tree; 30 | pub mod visitor; 31 | 32 | pub use crate::nth_index_cache::NthIndexCache; 33 | pub use crate::parser::{Parser, SelectorImpl, SelectorList}; 34 | pub use crate::tree::{Element, OpaqueElement}; 35 | -------------------------------------------------------------------------------- /src/rules/document.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use super::{CssRuleList, MinifyContext}; 3 | use crate::error::{MinifyError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::traits::ToCss; 6 | 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub struct MozDocumentRule<'i> { 9 | pub rules: CssRuleList<'i>, 10 | pub loc: Location, 11 | } 12 | 13 | impl<'i> MozDocumentRule<'i> { 14 | pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> { 15 | self.rules.minify(context, false) 16 | } 17 | } 18 | 19 | impl<'i> ToCss for MozDocumentRule<'i> { 20 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 21 | where 22 | W: std::fmt::Write, 23 | { 24 | dest.add_mapping(self.loc); 25 | dest.write_str("@-moz-document url-prefix()")?; 26 | dest.whitespace()?; 27 | dest.write_char('{')?; 28 | dest.indent(); 29 | dest.newline()?; 30 | self.rules.to_css(dest)?; 31 | dest.dedent(); 32 | dest.newline()?; 33 | dest.write_char('}') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/rules/nesting.rs: -------------------------------------------------------------------------------- 1 | use super::style::StyleRule; 2 | use super::Location; 3 | use super::MinifyContext; 4 | use crate::error::{MinifyError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::rules::{StyleContext, ToCssWithContext}; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct NestingRule<'i> { 10 | pub style: StyleRule<'i>, 11 | pub loc: Location, 12 | } 13 | 14 | impl<'i> NestingRule<'i> { 15 | pub(crate) fn minify( 16 | &mut self, 17 | context: &mut MinifyContext<'_, 'i>, 18 | parent_is_unused: bool, 19 | ) -> Result { 20 | self.style.minify(context, parent_is_unused) 21 | } 22 | } 23 | 24 | impl<'a, 'i> ToCssWithContext<'a, 'i> for NestingRule<'i> { 25 | fn to_css_with_context( 26 | &self, 27 | dest: &mut Printer, 28 | context: Option<&StyleContext<'a, 'i>>, 29 | ) -> Result<(), PrinterError> 30 | where 31 | W: std::fmt::Write, 32 | { 33 | dest.add_mapping(self.loc); 34 | if context.is_none() { 35 | dest.write_str("@nest ")?; 36 | } 37 | self.style.to_css_with_context(dest, context) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | use cssparser::*; 8 | 9 | /// A CSS [``](https://www.w3.org/TR/css-color-4/#typedef-alpha-value), 10 | /// used to represent opacity. 11 | /// 12 | /// Parses either a `` or ``, but is always stored and serialized as a number. 13 | #[derive(Debug, Clone, PartialEq)] 14 | pub struct AlphaValue(pub f32); 15 | 16 | impl<'i> Parse<'i> for AlphaValue { 17 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 18 | match NumberOrPercentage::parse(input)? { 19 | NumberOrPercentage::Percentage(percent) => Ok(AlphaValue(percent.0)), 20 | NumberOrPercentage::Number(number) => Ok(AlphaValue(number)), 21 | } 22 | } 23 | } 24 | 25 | impl ToCss for AlphaValue { 26 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 27 | where 28 | W: std::fmt::Write, 29 | { 30 | self.0.to_css(dest) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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::{Parse, ToCss}; 6 | use cssparser::*; 7 | 8 | /// A generic value that represents a value with two components, e.g. a border radius. 9 | /// 10 | /// When serialized, only a single component will be written if both are equal. 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub struct Size2D(pub T, pub T); 13 | 14 | impl<'i, T> Parse<'i> for Size2D 15 | where 16 | T: Parse<'i> + Clone, 17 | { 18 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 19 | let first = T::parse(input)?; 20 | let second = input.try_parse(T::parse).unwrap_or_else(|_| first.clone()); 21 | Ok(Size2D(first, second)) 22 | } 23 | } 24 | 25 | impl ToCss for Size2D 26 | where 27 | T: ToCss + PartialEq, 28 | { 29 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 30 | where 31 | W: std::fmt::Write, 32 | { 33 | self.0.to_css(dest)?; 34 | if self.1 != self.0 { 35 | dest.write_str(" ")?; 36 | self.1.to_css(dest)?; 37 | } 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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('parcel-css'); 30 | let res = css.transform(opts); 31 | console.timeEnd('parcel-css'); 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, family} = require('detect-libc'); 7 | if (family === MUSL) { 8 | parts.push('musl'); 9 | } else if (process.arch === 'arm') { 10 | parts.push('gnueabihf'); 11 | } else { 12 | parts.push('gnu'); 13 | } 14 | } else if (process.platform === 'win32') { 15 | parts.push('msvc'); 16 | } 17 | 18 | let binary = process.platform === 'win32' ? 'parcel_css.exe' : 'parcel_css'; 19 | 20 | let pkgPath; 21 | try { 22 | pkgPath = path.dirname(require.resolve(`@parcel/css-cli-${parts.join('-')}/package.json`)); 23 | } catch (err) { 24 | pkgPath = path.join(__dirname, '..', 'target', 'release'); 25 | if (!fs.existsSync(path.join(pkgPath, binary))) { 26 | pkgPath = path.join(__dirname, '..', 'target', 'debug'); 27 | } 28 | } 29 | 30 | try { 31 | fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, binary)); 32 | } catch (err) { 33 | try { 34 | fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, binary)); 35 | } catch (err) { 36 | console.error('Failed to move @parcel/css-cli binary into place.'); 37 | process.exit(1); 38 | } 39 | } 40 | 41 | if (process.platform === 'win32') { 42 | try { 43 | fs.unlinkSync(path.join(__dirname, 'parcel_css')); 44 | } catch (err) {} 45 | } 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/vendor_prefix.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | 3 | use crate::error::PrinterError; 4 | use crate::printer::Printer; 5 | use crate::traits::ToCss; 6 | use bitflags::bitflags; 7 | 8 | bitflags! { 9 | pub struct VendorPrefix: u8 { 10 | const None = 0b00000001; 11 | const WebKit = 0b00000010; 12 | const Moz = 0b00000100; 13 | const Ms = 0b00001000; 14 | const O = 0b00010000; 15 | } 16 | } 17 | 18 | impl Default for VendorPrefix { 19 | fn default() -> VendorPrefix { 20 | VendorPrefix::None 21 | } 22 | } 23 | 24 | impl VendorPrefix { 25 | pub fn from_str(s: &str) -> VendorPrefix { 26 | match s { 27 | "webkit" => VendorPrefix::WebKit, 28 | "moz" => VendorPrefix::Moz, 29 | "ms" => VendorPrefix::Ms, 30 | "o" => VendorPrefix::O, 31 | _ => unreachable!(), 32 | } 33 | } 34 | } 35 | 36 | impl ToCss for VendorPrefix { 37 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 38 | where 39 | W: std::fmt::Write, 40 | { 41 | cssparser::ToCss::to_css(self, dest)?; 42 | Ok(()) 43 | } 44 | } 45 | 46 | impl cssparser::ToCss for VendorPrefix { 47 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 48 | where 49 | W: std::fmt::Write, 50 | { 51 | match *self { 52 | VendorPrefix::WebKit => dest.write_str("-webkit-"), 53 | VendorPrefix::Moz => dest.write_str("-moz-"), 54 | VendorPrefix::Ms => dest.write_str("-ms-"), 55 | VendorPrefix::O => dest.write_str("-o-"), 56 | _ => Ok(()), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const css = require('./'); 2 | const fs = require('fs'); 3 | 4 | if (process.argv[process.argv.length - 1] !== __filename) { 5 | let opts = { 6 | filename: process.argv[process.argv.length - 1], 7 | code: fs.readFileSync(process.argv[process.argv.length - 1]), 8 | minify: true, 9 | sourceMap: true, 10 | targets: { 11 | chrome: 95 << 16 12 | } 13 | }; 14 | 15 | console.time('optimize'); 16 | let r = css.transform(opts); 17 | console.timeEnd('optimize') 18 | // console.log(r.toString()); 19 | console.log(r); 20 | let code = r.code; 21 | if (r.map) { 22 | code = code.toString() + `\n/*# sourceMappingURL=out.css.map */\n`; 23 | } 24 | fs.writeFileSync('out.css', code); 25 | if (r.map) { 26 | fs.writeFileSync('out.css.map', r.map); 27 | } 28 | return; 29 | } 30 | 31 | let res = css.transform({ 32 | filename: __filename, 33 | minify: false, 34 | targets: { 35 | safari: 4 << 16, 36 | firefox: 3 << 16 | 5 << 8, 37 | opera: 10 << 16 | 5 << 8 38 | }, 39 | code: Buffer.from(` 40 | @import "foo.css"; 41 | @import "bar.css" print; 42 | @import "baz.css" supports(display: grid); 43 | 44 | .foo { 45 | composes: bar; 46 | composes: baz from "baz.css"; 47 | color: pink; 48 | } 49 | 50 | .bar { 51 | color: red; 52 | background: url(test.jpg); 53 | } 54 | `), 55 | drafts: { 56 | nesting: true 57 | }, 58 | cssModules: true, 59 | analyzeDependencies: true 60 | }); 61 | 62 | console.log(res.code.toString()); 63 | console.log(res.exports); 64 | console.log(require('util').inspect(res.dependencies, { colors: true, depth: 50 })); 65 | -------------------------------------------------------------------------------- /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 parcel_css::{ 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(); 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/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 | use cssparser::*; 8 | 9 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#ratios) value, 10 | /// representing the ratio of two numeric values. 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub struct Ratio(pub CSSNumber, pub CSSNumber); 13 | 14 | impl<'i> Parse<'i> for Ratio { 15 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 16 | let first = CSSNumber::parse(input)?; 17 | let second = if input.try_parse(|input| input.expect_delim('/')).is_ok() { 18 | CSSNumber::parse(input)? 19 | } else { 20 | 1.0 21 | }; 22 | 23 | Ok(Ratio(first, second)) 24 | } 25 | } 26 | 27 | impl Ratio { 28 | /// Parses a ratio where both operands are required. 29 | pub fn parse_required<'i, 't>(input: &mut Parser<'i, 't>) -> Result>> { 30 | let first = CSSNumber::parse(input)?; 31 | input.expect_delim('/')?; 32 | let second = CSSNumber::parse(input)?; 33 | Ok(Ratio(first, second)) 34 | } 35 | } 36 | 37 | impl ToCss for Ratio { 38 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 39 | where 40 | W: std::fmt::Write, 41 | { 42 | self.0.to_css(dest)?; 43 | if self.1 != 1.0 { 44 | dest.delim('/', true)?; 45 | self.1.to_css(dest)?; 46 | } 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl std::ops::Add for Ratio { 52 | type Output = Self; 53 | 54 | fn add(self, other: CSSNumber) -> Ratio { 55 | Ratio(self.0 + other, self.1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | extern crate phf_codegen; 6 | 7 | use std::env; 8 | use std::fs::File; 9 | use std::io::{BufWriter, Write}; 10 | use std::path::Path; 11 | 12 | fn main() { 13 | let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("ascii_case_insensitive_html_attributes.rs"); 14 | let mut file = BufWriter::new(File::create(&path).unwrap()); 15 | 16 | let mut set = phf_codegen::Set::new(); 17 | for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { 18 | set.entry(name); 19 | } 20 | write!( 21 | &mut file, 22 | "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}", 23 | set.build(), 24 | ) 25 | .unwrap(); 26 | } 27 | 28 | /// 29 | static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#" 30 | accept 31 | accept-charset 32 | align 33 | alink 34 | axis 35 | bgcolor 36 | charset 37 | checked 38 | clear 39 | codetype 40 | color 41 | compact 42 | declare 43 | defer 44 | dir 45 | direction 46 | disabled 47 | enctype 48 | face 49 | frame 50 | hreflang 51 | http-equiv 52 | lang 53 | language 54 | link 55 | media 56 | method 57 | multiple 58 | nohref 59 | noresize 60 | noshade 61 | nowrap 62 | readonly 63 | rel 64 | rev 65 | rules 66 | scope 67 | scrolling 68 | selected 69 | shape 70 | target 71 | text 72 | type 73 | valign 74 | valuetype 75 | vlink 76 | "#; 77 | -------------------------------------------------------------------------------- /src/rules/import.rs: -------------------------------------------------------------------------------- 1 | use super::layer::LayerName; 2 | use super::supports::SupportsCondition; 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::string::CowArcStr; 9 | use cssparser::*; 10 | 11 | /// https://drafts.csswg.org/css-cascade/#at-import 12 | #[derive(Debug, PartialEq, Clone)] 13 | pub struct ImportRule<'i> { 14 | pub url: CowArcStr<'i>, 15 | pub layer: Option>>, 16 | pub supports: Option>, 17 | pub media: MediaList<'i>, 18 | pub loc: Location, 19 | } 20 | 21 | impl<'i> ToCss for ImportRule<'i> { 22 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 23 | where 24 | W: std::fmt::Write, 25 | { 26 | dest.add_mapping(self.loc); 27 | dest.write_str("@import ")?; 28 | serialize_string(&self.url, dest)?; 29 | 30 | if let Some(layer) = &self.layer { 31 | dest.write_str(" layer")?; 32 | if let Some(name) = layer { 33 | dest.write_char('(')?; 34 | name.to_css(dest)?; 35 | dest.write_char(')')?; 36 | } 37 | } 38 | 39 | if let Some(supports) = &self.supports { 40 | dest.write_str(" supports")?; 41 | if matches!( 42 | supports, 43 | SupportsCondition::Declaration(_) | SupportsCondition::Parens(_) 44 | ) { 45 | supports.to_css(dest)?; 46 | } else { 47 | dest.write_char('(')?; 48 | supports.to_css(dest)?; 49 | dest.write_char(')')?; 50 | } 51 | } 52 | if !self.media.media_queries.is_empty() { 53 | dest.write_char(' ')?; 54 | self.media.to_css(dest)?; 55 | } 56 | dest.write_str(";") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 fxhash::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 | -------------------------------------------------------------------------------- /src/rules/media.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use super::{CssRuleList, MinifyContext}; 3 | use crate::error::{MinifyError, PrinterError}; 4 | use crate::media_query::MediaList; 5 | use crate::printer::Printer; 6 | use crate::rules::{StyleContext, ToCssWithContext}; 7 | use crate::traits::ToCss; 8 | 9 | #[derive(Debug, PartialEq, Clone)] 10 | pub struct MediaRule<'i> { 11 | pub query: MediaList<'i>, 12 | pub rules: CssRuleList<'i>, 13 | pub loc: Location, 14 | } 15 | 16 | impl<'i> MediaRule<'i> { 17 | pub(crate) fn minify( 18 | &mut self, 19 | context: &mut MinifyContext<'_, 'i>, 20 | parent_is_unused: bool, 21 | ) -> Result { 22 | self.rules.minify(context, parent_is_unused)?; 23 | 24 | if let Some(custom_media) = &context.custom_media { 25 | self.query.transform_custom_media(self.loc, custom_media)?; 26 | } 27 | 28 | Ok(self.rules.0.is_empty() || self.query.never_matches()) 29 | } 30 | } 31 | 32 | impl<'a, 'i> ToCssWithContext<'a, 'i> for MediaRule<'i> { 33 | fn to_css_with_context( 34 | &self, 35 | dest: &mut Printer, 36 | context: Option<&StyleContext<'a, 'i>>, 37 | ) -> Result<(), PrinterError> 38 | where 39 | W: std::fmt::Write, 40 | { 41 | // If the media query always matches, we can just output the nested rules. 42 | if dest.minify && self.query.always_matches() { 43 | self.rules.to_css_with_context(dest, context)?; 44 | return Ok(()); 45 | } 46 | 47 | dest.add_mapping(self.loc); 48 | dest.write_str("@media ")?; 49 | self.query.to_css(dest)?; 50 | dest.whitespace()?; 51 | dest.write_char('{')?; 52 | dest.indent(); 53 | dest.newline()?; 54 | self.rules.to_css_with_context(dest, context)?; 55 | dest.dedent(); 56 | dest.newline()?; 57 | dest.write_char('}') 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "node", 4 | "selectors" 5 | ] 6 | 7 | [package] 8 | authors = ["Devon Govett "] 9 | name = "parcel_css" 10 | version = "1.0.0-alpha.22" 11 | description = "A CSS parser, transformer, and minifier" 12 | license = "MPL-2.0" 13 | edition = "2021" 14 | keywords = [ "CSS", "minifier", "Parcel" ] 15 | repository = "https://github.com/parcel-bundler/parcel-css" 16 | 17 | [[bin]] 18 | name = "parcel_css" 19 | path = "src/main.rs" 20 | required-features = ["cli"] 21 | 22 | [lib] 23 | name = "parcel_css" 24 | path = "src/lib.rs" 25 | crate-type = ["rlib"] 26 | 27 | [dependencies] 28 | serde = { version = "1.0.123", features = ["derive"] } 29 | cssparser = "0.29.1" 30 | parcel_selectors = { version = "0.24.4", path = "./selectors" } 31 | itertools = "0.10.1" 32 | smallvec = { version = "1.7.0", features = ["union"] } 33 | bitflags = "1.3.2" 34 | parcel_sourcemap = "2.0.2" 35 | data-encoding = "2.3.2" 36 | lazy_static = "1.4.0" 37 | retain_mut = "0.1.5" 38 | const-str = "0.3.1" 39 | # CLI deps 40 | clap = { version = "3.0.6", features = ["derive"], optional = true } 41 | serde_json = { version = "1.0.78", optional = true } 42 | pathdiff = { version = "0.2.1", optional = true } 43 | browserslist-rs = { version = "0.7.0", optional = true } 44 | rayon = "1.5.1" 45 | dashmap = "5.0.0" 46 | 47 | [target.'cfg(target_os = "macos")'.dependencies] 48 | jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } 49 | 50 | [dev-dependencies] 51 | indoc = "1.0.3" 52 | assert_cmd = "2.0" 53 | assert_fs = "1.0" 54 | predicates = "2.1" 55 | 56 | [features] 57 | default = ["grid"] 58 | cli = ["clap", "serde_json", "pathdiff", "browserslist-rs"] 59 | grid = [] 60 | 61 | [[test]] 62 | name = "cli_integration_tests" 63 | path = "tests/cli_integration_tests.rs" 64 | required-features = ["cli"] 65 | 66 | [profile.release] 67 | lto = true 68 | codegen-units = 1 69 | panic = 'abort' 70 | -------------------------------------------------------------------------------- /scripts/build-wasm.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').execSync; 2 | const fs = require('fs'); 3 | const pkg = require('../package.json'); 4 | 5 | const dir = `${__dirname}/..`; 6 | 7 | try { 8 | fs.mkdirSync(dir + '/npm'); 9 | } catch (err) {} 10 | 11 | exec(`cp -R ${dir}/artifacts/wasm ${dir}/npm/.`); 12 | fs.writeFileSync(`${dir}/npm/wasm/index.js`, `export {default, transform, transformStyleAttribute} from './parcel_css_node.js';\nexport {browserslistToTargets} from './browserslistToTargets.js'`); 13 | 14 | let b = fs.readFileSync(`${dir}/node/browserslistToTargets.js`, 'utf8'); 15 | b = b.replace('module.exports = browserslistToTargets;', 'export {browserslistToTargets};'); 16 | fs.writeFileSync(`${dir}/npm/wasm/browserslistToTargets.js`, b); 17 | fs.unlinkSync(`${dir}/npm/wasm/parcel_css_node.d.ts`); 18 | 19 | let dts = fs.readFileSync(`${dir}/node/index.d.ts`, 'utf8'); 20 | dts = dts.replace(/: Buffer/g, ': Uint8Array'); 21 | dts += ` 22 | /** Initializes the web assembly module. */ 23 | export default function init(): Promise; 24 | `; 25 | fs.writeFileSync(`${dir}/npm/wasm/index.d.ts`, dts); 26 | fs.copyFileSync(`${dir}/node/targets.d.ts`, `${dir}/npm/wasm/targets.d.ts`); 27 | 28 | let readme = fs.readFileSync(`${dir}/README.md`, 'utf8'); 29 | readme = readme.replace('# @parcel/css', '# @parcel/css-wasm'); 30 | fs.writeFileSync(`${dir}/npm/wasm/README.md`, readme); 31 | 32 | fs.unlinkSync(`${dir}/npm/wasm/.gitignore`); 33 | 34 | let wasmPkg = {...pkg}; 35 | wasmPkg.name = '@parcel/css-wasm'; 36 | wasmPkg.module = 'index.js'; 37 | wasmPkg.types = 'index.d.ts'; 38 | wasmPkg.sideEffects = false; 39 | delete wasmPkg.main; 40 | delete wasmPkg.files; 41 | delete wasmPkg.napi; 42 | delete wasmPkg.devDependencies; 43 | delete wasmPkg.dependencies; 44 | delete wasmPkg.optionalDependencies; 45 | delete wasmPkg.targets; 46 | delete wasmPkg.scripts; 47 | fs.writeFileSync(`${dir}/npm/wasm/package.json`, JSON.stringify(wasmPkg, false, 2) + '\n'); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@parcel/css", 3 | "version": "1.8.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 | "targets": { 9 | "main": false, 10 | "types": false 11 | }, 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "funding": { 16 | "type": "opencollective", 17 | "url": "https://opencollective.com/parcel" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/parcel-bundler/parcel-css.git" 22 | }, 23 | "engines": { 24 | "node": ">= 12.0.0" 25 | }, 26 | "napi": { 27 | "name": "parcel-css" 28 | }, 29 | "files": [ 30 | "node/*.js", 31 | "node/*.d.ts", 32 | "node/*.flow" 33 | ], 34 | "dependencies": { 35 | "detect-libc": "^1.0.3" 36 | }, 37 | "devDependencies": { 38 | "@mdn/browser-compat-data": "^4.1.12", 39 | "@napi-rs/cli": "1.0.4", 40 | "autoprefixer": "^10.4.4", 41 | "caniuse-lite": "^1.0.30001319", 42 | "cssnano": "^5.0.8", 43 | "esbuild": "^0.13.10", 44 | "jest-diff": "^27.4.2", 45 | "node-fetch": "^3.1.0", 46 | "parcel": "^2.0.1", 47 | "postcss": "^8.3.11", 48 | "puppeteer": "^12.0.1" 49 | }, 50 | "scripts": { 51 | "build": "node scripts/build.js && node scripts/build-flow.js", 52 | "build-release": "node scripts/build.js --release && node scripts/build-flow.js", 53 | "prepublishOnly": "node scripts/build-flow.js", 54 | "wasm:build": "wasm-pack build node --target nodejs", 55 | "wasm:build-release": "wasm-pack build node --target nodejs --release", 56 | "wasm-browser:build": "wasm-pack build node --target web", 57 | "wasm-browser:build-release": "wasm-pack build node --target web --release", 58 | "playground:start": "parcel playground/index.html", 59 | "playground:build": "yarn wasm-browser:build-release && parcel build playground/index.html" 60 | } 61 | } -------------------------------------------------------------------------------- /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/properties/outline.rs: -------------------------------------------------------------------------------- 1 | //! CSS properties related to outlines. 2 | 3 | use super::border::{BorderSideWidth, BorderStyle, GenericBorder}; 4 | use super::{Property, PropertyId}; 5 | use crate::context::PropertyHandlerContext; 6 | use crate::declaration::DeclarationList; 7 | use crate::error::{ParserError, PrinterError}; 8 | use crate::macros::shorthand_handler; 9 | use crate::printer::Printer; 10 | use crate::targets::Browsers; 11 | use crate::traits::{FallbackValues, Parse, PropertyHandler, ToCss}; 12 | use crate::values::color::CssColor; 13 | use cssparser::*; 14 | 15 | /// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property. 16 | #[derive(Debug, Clone, PartialEq)] 17 | pub enum OutlineStyle { 18 | /// The `auto` keyword. 19 | Auto, 20 | /// A value equivalent to the `border-style` property. 21 | BorderStyle(BorderStyle), 22 | } 23 | 24 | impl<'i> Parse<'i> for OutlineStyle { 25 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 26 | if let Ok(border_style) = input.try_parse(BorderStyle::parse) { 27 | return Ok(OutlineStyle::BorderStyle(border_style)); 28 | } 29 | 30 | input.expect_ident_matching("auto")?; 31 | Ok(OutlineStyle::Auto) 32 | } 33 | } 34 | 35 | impl ToCss for OutlineStyle { 36 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 37 | where 38 | W: std::fmt::Write, 39 | { 40 | match self { 41 | OutlineStyle::Auto => dest.write_str("auto"), 42 | OutlineStyle::BorderStyle(border_style) => border_style.to_css(dest), 43 | } 44 | } 45 | } 46 | 47 | impl Default for OutlineStyle { 48 | fn default() -> OutlineStyle { 49 | OutlineStyle::BorderStyle(BorderStyle::None) 50 | } 51 | } 52 | 53 | /// A value for the [outline](https://drafts.csswg.org/css-ui/#outline) shorthand property. 54 | pub type Outline = GenericBorder; 55 | 56 | shorthand_handler!(OutlineHandler -> Outline { 57 | width: OutlineWidth(BorderSideWidth), 58 | style: OutlineStyle(OutlineStyle), 59 | color: OutlineColor(CssColor, fallback: true), 60 | }); 61 | -------------------------------------------------------------------------------- /src/values/resolution.rs: -------------------------------------------------------------------------------- 1 | //! CSS resolution values. 2 | 3 | use super::length::serialize_dimension; 4 | use super::number::CSSNumber; 5 | use crate::error::{ParserError, PrinterError}; 6 | use crate::printer::Printer; 7 | use crate::traits::{Parse, ToCss}; 8 | use cssparser::*; 9 | 10 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#resolution) value. 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub enum Resolution { 13 | /// A resolution in dots per inch. 14 | Dpi(CSSNumber), 15 | /// A resolution in dots per centimeter. 16 | Dpcm(CSSNumber), 17 | /// A resolution in dots per px. 18 | Dppx(CSSNumber), 19 | } 20 | 21 | impl<'i> Parse<'i> for Resolution { 22 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 23 | // TODO: calc? 24 | let location = input.current_source_location(); 25 | match *input.next()? { 26 | Token::Dimension { value, ref unit, .. } => { 27 | match_ignore_ascii_case! { unit, 28 | "dpi" => Ok(Resolution::Dpi(value)), 29 | "dpcm" => Ok(Resolution::Dpcm(value)), 30 | "dppx" | "x" => Ok(Resolution::Dppx(value)), 31 | _ => Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) 32 | } 33 | } 34 | ref t => Err(location.new_unexpected_token_error(t.clone())), 35 | } 36 | } 37 | } 38 | 39 | impl ToCss for Resolution { 40 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 41 | where 42 | W: std::fmt::Write, 43 | { 44 | let (value, unit) = match self { 45 | Resolution::Dpi(dpi) => (*dpi, "dpi"), 46 | Resolution::Dpcm(dpcm) => (*dpcm, "dpcm"), 47 | Resolution::Dppx(dppx) => (*dppx, "x"), 48 | }; 49 | 50 | serialize_dimension(value, unit, dest) 51 | } 52 | } 53 | 54 | impl std::ops::Add for Resolution { 55 | type Output = Self; 56 | 57 | fn add(self, other: CSSNumber) -> Resolution { 58 | match self { 59 | Resolution::Dpi(dpi) => Resolution::Dpi(dpi + other), 60 | Resolution::Dpcm(dpcm) => Resolution::Dpcm(dpcm + other), 61 | Resolution::Dppx(dppx) => Resolution::Dppx(dppx + other), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::context::PropertyHandlerContext; 2 | use crate::declaration::DeclarationList; 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::properties::Property; 6 | use crate::stylesheet::PrinterOptions; 7 | use crate::targets::Browsers; 8 | use cssparser::*; 9 | 10 | /// Trait for things that can be parsed from CSS syntax. 11 | pub trait Parse<'i>: Sized { 12 | /// Parse a value of this type using an existing parser. 13 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>>; 14 | 15 | /// Parse a value from a string. 16 | /// 17 | /// (This is a convenience wrapper for `parse` and probably should not be overridden.) 18 | fn parse_string(input: &'i str) -> Result>> { 19 | let mut input = ParserInput::new(input); 20 | let mut parser = Parser::new(&mut input); 21 | let result = Self::parse(&mut parser)?; 22 | parser.expect_exhausted()?; 23 | Ok(result) 24 | } 25 | } 26 | 27 | /// Trait for things the can serialize themselves in CSS syntax. 28 | pub trait ToCss { 29 | /// Serialize `self` in CSS syntax, writing to `dest`. 30 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 31 | where 32 | W: std::fmt::Write; 33 | 34 | /// Serialize `self` in CSS syntax and return a string. 35 | /// 36 | /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) 37 | #[inline] 38 | fn to_css_string(&self, options: PrinterOptions) -> Result { 39 | let mut s = String::new(); 40 | let mut printer = Printer::new(&mut s, options); 41 | self.to_css(&mut printer)?; 42 | Ok(s) 43 | } 44 | } 45 | 46 | impl<'a, T> ToCss for &'a T 47 | where 48 | T: ToCss + ?Sized, 49 | { 50 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 51 | where 52 | W: std::fmt::Write, 53 | { 54 | (*self).to_css(dest) 55 | } 56 | } 57 | 58 | pub(crate) trait PropertyHandler<'i>: Sized { 59 | fn handle_property( 60 | &mut self, 61 | property: &Property<'i>, 62 | dest: &mut DeclarationList<'i>, 63 | context: &mut PropertyHandlerContext<'i>, 64 | ) -> bool; 65 | fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i>); 66 | } 67 | 68 | pub(crate) mod private { 69 | pub trait TryAdd { 70 | fn try_add(&self, other: &T) -> Option; 71 | } 72 | } 73 | 74 | pub(crate) trait FromStandard: Sized { 75 | fn from_standard(val: &T) -> Option; 76 | } 77 | 78 | pub(crate) trait FallbackValues: Sized { 79 | fn get_fallbacks(&mut self, targets: Browsers) -> Vec; 80 | } 81 | -------------------------------------------------------------------------------- /src/values/ident.rs: -------------------------------------------------------------------------------- 1 | //! CSS identifiers. 2 | 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::traits::{Parse, ToCss}; 6 | use crate::values::string::CowArcStr; 7 | use cssparser::*; 8 | use smallvec::SmallVec; 9 | 10 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#custom-idents). 11 | /// 12 | /// Custom idents are author defined, and allow any valid identifier except the 13 | /// [CSS-wide keywords](https://www.w3.org/TR/css-values-4/#css-wide-keywords). 14 | /// They may be renamed to include a hash when compiled as part of a CSS module. 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 16 | pub struct CustomIdent<'i>(pub CowArcStr<'i>); 17 | 18 | impl<'i> Parse<'i> for CustomIdent<'i> { 19 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 20 | let location = input.current_source_location(); 21 | let ident = input.expect_ident()?; 22 | let valid = match_ignore_ascii_case! { &ident, 23 | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => false, 24 | _ => true 25 | }; 26 | 27 | if !valid { 28 | return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))); 29 | } 30 | 31 | Ok(CustomIdent(ident.into())) 32 | } 33 | } 34 | 35 | impl<'i> ToCss for CustomIdent<'i> { 36 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 37 | where 38 | W: std::fmt::Write, 39 | { 40 | dest.write_ident(&self.0) 41 | } 42 | } 43 | 44 | /// A list of CSS [``](https://www.w3.org/TR/css-values-4/#custom-idents) values. 45 | pub type CustomIdentList<'i> = SmallVec<[CustomIdent<'i>; 1]>; 46 | 47 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#dashed-idents). 48 | /// 49 | /// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. 50 | /// Author defined idents must start with two dash characters ("--") or parsing will fail. 51 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 52 | pub struct DashedIdent<'i>(pub CowArcStr<'i>); 53 | 54 | impl<'i> Parse<'i> for DashedIdent<'i> { 55 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 56 | let location = input.current_source_location(); 57 | let ident = input.expect_ident()?; 58 | if !ident.starts_with("--") { 59 | return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))); 60 | } 61 | 62 | Ok(DashedIdent(ident.into())) 63 | } 64 | } 65 | 66 | impl<'i> ToCss for DashedIdent<'i> { 67 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 68 | where 69 | W: std::fmt::Write, 70 | { 71 | dest.write_ident(&self.0) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crate::css_modules::hash; 2 | use crate::printer::PrinterOptions; 3 | use crate::rules::import::ImportRule; 4 | use crate::traits::ToCss; 5 | use crate::values::url::Url; 6 | use cssparser::SourceLocation; 7 | use serde::Serialize; 8 | 9 | #[derive(Serialize)] 10 | #[serde(tag = "type", rename_all = "lowercase")] 11 | pub enum Dependency { 12 | Import(ImportDependency), 13 | Url(UrlDependency), 14 | } 15 | 16 | #[derive(Serialize)] 17 | pub struct ImportDependency { 18 | pub url: String, 19 | pub supports: Option, 20 | pub media: Option, 21 | pub loc: SourceRange, 22 | } 23 | 24 | impl ImportDependency { 25 | pub fn new(rule: &ImportRule, filename: &str) -> ImportDependency { 26 | let supports = if let Some(supports) = &rule.supports { 27 | let s = supports.to_css_string(PrinterOptions::default()).unwrap(); 28 | Some(s) 29 | } else { 30 | None 31 | }; 32 | 33 | let media = if !rule.media.media_queries.is_empty() { 34 | let s = rule.media.to_css_string(PrinterOptions::default()).unwrap(); 35 | Some(s) 36 | } else { 37 | None 38 | }; 39 | 40 | ImportDependency { 41 | url: rule.url.as_ref().to_owned(), 42 | supports, 43 | media, 44 | loc: SourceRange::new( 45 | filename, 46 | SourceLocation { 47 | line: rule.loc.line, 48 | column: rule.loc.column, 49 | }, 50 | 8, 51 | rule.url.len() + 2, 52 | ), // TODO: what about @import url(...)? 53 | } 54 | } 55 | } 56 | 57 | #[derive(Serialize)] 58 | pub struct UrlDependency { 59 | pub url: String, 60 | pub placeholder: String, 61 | pub loc: SourceRange, 62 | } 63 | 64 | impl UrlDependency { 65 | pub fn new(url: &Url, filename: &str) -> UrlDependency { 66 | let placeholder = hash(&format!("{}_{}", filename, url.url)); 67 | UrlDependency { 68 | url: url.url.to_string(), 69 | placeholder, 70 | loc: SourceRange::new(filename, url.loc, 4, url.url.len()), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Serialize)] 76 | #[serde(rename_all = "camelCase")] 77 | pub struct SourceRange { 78 | pub file_path: String, 79 | pub start: Location, 80 | pub end: Location, 81 | } 82 | 83 | #[derive(Serialize)] 84 | pub struct Location { 85 | pub line: u32, 86 | pub column: u32, 87 | } 88 | 89 | impl SourceRange { 90 | fn new(filename: &str, loc: SourceLocation, offset: u32, len: usize) -> SourceRange { 91 | SourceRange { 92 | file_path: filename.into(), 93 | start: Location { 94 | line: loc.line + 1, 95 | column: loc.column + offset, 96 | }, 97 | end: Location { 98 | line: loc.line + 1, 99 | column: loc.column + offset + (len as u32) - 1, 100 | }, 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/properties/css_modules.rs: -------------------------------------------------------------------------------- 1 | //! Properties related to CSS modules. 2 | 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::printer::Printer; 5 | use crate::traits::{Parse, ToCss}; 6 | use crate::values::ident::{CustomIdent, CustomIdentList}; 7 | use crate::values::string::CowArcStr; 8 | use cssparser::*; 9 | use smallvec::SmallVec; 10 | 11 | /// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules. 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct Composes<'i> { 14 | /// A list of class names to compose. 15 | pub names: CustomIdentList<'i>, 16 | /// Where the class names are composed from. 17 | pub from: Option>, 18 | /// The source location of the `composes` property. 19 | pub loc: SourceLocation, 20 | } 21 | 22 | /// Defines where the class names referenced in the `composes` property are located. 23 | /// 24 | /// See [Composes](Composes). 25 | #[derive(Debug, Clone, PartialEq)] 26 | pub enum ComposesFrom<'i> { 27 | /// The class name is global. 28 | Global, 29 | /// The class name comes from the specified file. 30 | File(CowArcStr<'i>), 31 | } 32 | 33 | impl<'i> Parse<'i> for Composes<'i> { 34 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 35 | let loc = input.current_source_location(); 36 | let mut names = SmallVec::new(); 37 | while let Ok(name) = input.try_parse(parse_one_ident) { 38 | names.push(name); 39 | } 40 | 41 | if names.is_empty() { 42 | return Err(input.new_custom_error(ParserError::InvalidDeclaration)); 43 | } 44 | 45 | let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() { 46 | if let Ok(file) = input.try_parse(|input| input.expect_string_cloned()) { 47 | Some(ComposesFrom::File(file.into())) 48 | } else { 49 | input.expect_ident_matching("global")?; 50 | Some(ComposesFrom::Global) 51 | } 52 | } else { 53 | None 54 | }; 55 | 56 | Ok(Composes { names, from, loc }) 57 | } 58 | } 59 | 60 | fn parse_one_ident<'i, 't>( 61 | input: &mut Parser<'i, 't>, 62 | ) -> Result, ParseError<'i, ParserError<'i>>> { 63 | let name = CustomIdent::parse(input)?; 64 | if name.0.eq_ignore_ascii_case("from") { 65 | return Err(input.new_error_for_next_token()); 66 | } 67 | 68 | Ok(name) 69 | } 70 | 71 | impl ToCss for Composes<'_> { 72 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 73 | where 74 | W: std::fmt::Write, 75 | { 76 | let mut first = true; 77 | for name in &self.names { 78 | if first { 79 | first = false; 80 | } else { 81 | dest.write_char(' ')?; 82 | } 83 | name.to_css(dest)?; 84 | } 85 | 86 | if let Some(from) = &self.from { 87 | dest.write_str(" from ")?; 88 | match from { 89 | ComposesFrom::Global => dest.write_str("global")?, 90 | ComposesFrom::File(file) => serialize_string(&file, dest)?, 91 | } 92 | } 93 | 94 | Ok(()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/rules/page.rs: -------------------------------------------------------------------------------- 1 | use super::Location; 2 | use crate::declaration::DeclarationBlock; 3 | use crate::error::{ParserError, PrinterError}; 4 | use crate::macros::enum_property; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | use crate::values::string::CowArcStr; 8 | use cssparser::*; 9 | 10 | /// https://www.w3.org/TR/css-page-3/#typedef-page-selector 11 | #[derive(Debug, PartialEq, Clone)] 12 | pub struct PageSelector<'i> { 13 | pub name: Option>, 14 | pub pseudo_classes: Vec, 15 | } 16 | 17 | enum_property! { 18 | pub enum PagePseudoClass { 19 | Left, 20 | Right, 21 | First, 22 | Last, 23 | Blank, 24 | } 25 | } 26 | 27 | impl<'i> Parse<'i> for PageSelector<'i> { 28 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 29 | let name = input.try_parse(|input| input.expect_ident().map(|x| x.into())).ok(); 30 | let mut pseudo_classes = vec![]; 31 | 32 | loop { 33 | // Whitespace is not allowed between pseudo classes 34 | let state = input.state(); 35 | match input.next_including_whitespace() { 36 | Ok(Token::Colon) => { 37 | pseudo_classes.push(PagePseudoClass::parse(input)?); 38 | } 39 | _ => { 40 | input.reset(&state); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | if name.is_none() && pseudo_classes.is_empty() { 47 | return Err(input.new_custom_error(ParserError::InvalidPageSelector)); 48 | } 49 | 50 | Ok(PageSelector { name, pseudo_classes }) 51 | } 52 | } 53 | 54 | #[derive(Debug, PartialEq, Clone)] 55 | pub struct PageRule<'i> { 56 | pub selectors: Vec>, 57 | pub declarations: DeclarationBlock<'i>, 58 | pub loc: Location, 59 | } 60 | 61 | impl<'i> ToCss for PageRule<'i> { 62 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 63 | where 64 | W: std::fmt::Write, 65 | { 66 | dest.add_mapping(self.loc); 67 | dest.write_str("@page")?; 68 | if let Some(first) = self.selectors.first() { 69 | // Space is only required if the first selector has a name. 70 | if !dest.minify || first.name.is_some() { 71 | dest.write_char(' ')?; 72 | } 73 | let mut first = true; 74 | for selector in &self.selectors { 75 | if first { 76 | first = false; 77 | } else { 78 | dest.delim(',', false)?; 79 | } 80 | selector.to_css(dest)?; 81 | } 82 | } 83 | self.declarations.to_css(dest) 84 | } 85 | } 86 | 87 | impl<'i> ToCss for PageSelector<'i> { 88 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 89 | where 90 | W: std::fmt::Write, 91 | { 92 | if let Some(name) = &self.name { 93 | dest.write_str(&name)?; 94 | } 95 | 96 | for pseudo in &self.pseudo_classes { 97 | dest.write_char(':')?; 98 | pseudo.to_css(dest)?; 99 | } 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/values/number.rs: -------------------------------------------------------------------------------- 1 | //! CSS number values. 2 | 3 | use super::calc::Calc; 4 | use crate::error::{ParserError, PrinterError}; 5 | use crate::printer::Printer; 6 | use crate::traits::{Parse, ToCss}; 7 | use cssparser::*; 8 | 9 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#numbers) value. 10 | /// 11 | /// Numbers may be explicit or computed by `calc()`, but are always stored and serialized 12 | /// as their computed value. 13 | pub type CSSNumber = f32; 14 | 15 | impl<'i> Parse<'i> for CSSNumber { 16 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 17 | match input.try_parse(Calc::parse) { 18 | Ok(Calc::Value(v)) => return Ok(*v), 19 | Ok(Calc::Number(n)) => return Ok(n), 20 | // Numbers are always compatible, so they will always compute to a value. 21 | Ok(_) => unreachable!(), 22 | _ => {} 23 | } 24 | 25 | let number = input.expect_number()?; 26 | Ok(number) 27 | } 28 | } 29 | 30 | impl ToCss for CSSNumber { 31 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 32 | where 33 | W: std::fmt::Write, 34 | { 35 | use cssparser::ToCss; 36 | let number = *self; 37 | let int_value = if number.fract() == 0.0 { 38 | Some(number as i32) 39 | } else { 40 | None 41 | }; 42 | let tok = Token::Number { 43 | has_sign: number < 0.0, 44 | value: number, 45 | int_value, 46 | }; 47 | if number != 0.0 && number.abs() < 1.0 { 48 | let mut s = String::new(); 49 | tok.to_css(&mut s)?; 50 | if number < 0.0 { 51 | dest.write_char('-')?; 52 | dest.write_str(s.trim_start_matches("-0")) 53 | } else { 54 | dest.write_str(s.trim_start_matches('0')) 55 | } 56 | } else { 57 | tok.to_css(dest)?; 58 | Ok(()) 59 | } 60 | } 61 | } 62 | 63 | impl std::convert::Into> for CSSNumber { 64 | fn into(self) -> Calc { 65 | Calc::Value(Box::new(self)) 66 | } 67 | } 68 | 69 | impl std::convert::From> for CSSNumber { 70 | fn from(calc: Calc) -> CSSNumber { 71 | match calc { 72 | Calc::Value(v) => *v, 73 | _ => unreachable!(), 74 | } 75 | } 76 | } 77 | 78 | /// A CSS [``](https://www.w3.org/TR/css-values-4/#integers) value. 79 | pub type CSSInteger = i32; 80 | 81 | impl<'i> Parse<'i> for CSSInteger { 82 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 83 | // TODO: calc?? 84 | let integer = input.expect_integer()?; 85 | Ok(integer) 86 | } 87 | } 88 | 89 | impl ToCss for CSSInteger { 90 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 91 | where 92 | W: std::fmt::Write, 93 | { 94 | use cssparser::ToCss; 95 | let integer = *self; 96 | let tok = Token::Number { 97 | has_sign: integer < 0, 98 | value: integer as f32, 99 | int_value: Some(integer), 100 | }; 101 | tok.to_css(dest)?; 102 | Ok(()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/rules/layer.rs: -------------------------------------------------------------------------------- 1 | use super::{CssRuleList, Location}; 2 | use crate::error::{ParserError, PrinterError}; 3 | use crate::printer::Printer; 4 | use crate::traits::{Parse, ToCss}; 5 | use crate::values::string::CowArcStr; 6 | use cssparser::*; 7 | use smallvec::SmallVec; 8 | 9 | /// https://drafts.csswg.org/css-cascade-5/#typedef-layer-name 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct LayerName<'i>(pub SmallVec<[CowArcStr<'i>; 1]>); 12 | 13 | macro_rules! expect_non_whitespace { 14 | ($parser: ident, $($branches: tt)+) => {{ 15 | let start_location = $parser.current_source_location(); 16 | match *$parser.next_including_whitespace()? { 17 | $($branches)+ 18 | ref token => { 19 | return Err(start_location.new_basic_unexpected_token_error(token.clone())) 20 | } 21 | } 22 | }} 23 | } 24 | 25 | impl<'i> Parse<'i> for LayerName<'i> { 26 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 27 | let mut parts = SmallVec::new(); 28 | let ident = input.expect_ident()?; 29 | parts.push(ident.into()); 30 | 31 | loop { 32 | let name = input.try_parse(|input| { 33 | expect_non_whitespace! {input, 34 | Token::Delim('.') => Ok(()), 35 | }?; 36 | 37 | expect_non_whitespace! {input, 38 | Token::Ident(ref id) => Ok(id.into()), 39 | } 40 | }); 41 | 42 | match name { 43 | Ok(name) => parts.push(name), 44 | Err(_) => break, 45 | } 46 | } 47 | 48 | Ok(LayerName(parts)) 49 | } 50 | } 51 | 52 | impl<'i> ToCss for LayerName<'i> { 53 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 54 | where 55 | W: std::fmt::Write, 56 | { 57 | let mut first = true; 58 | for name in &self.0 { 59 | if first { 60 | first = false; 61 | } else { 62 | dest.write_char('.')?; 63 | } 64 | 65 | dest.write_str(name)?; 66 | } 67 | 68 | Ok(()) 69 | } 70 | } 71 | 72 | /// https://drafts.csswg.org/css-cascade-5/#layer-empty 73 | #[derive(Debug, Clone, PartialEq)] 74 | pub struct LayerStatementRule<'i> { 75 | pub names: Vec>, 76 | pub loc: Location, 77 | } 78 | 79 | impl<'i> ToCss for LayerStatementRule<'i> { 80 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 81 | where 82 | W: std::fmt::Write, 83 | { 84 | dest.add_mapping(self.loc); 85 | dest.write_str("@layer ")?; 86 | self.names.to_css(dest)?; 87 | dest.write_char(';') 88 | } 89 | } 90 | 91 | /// https://drafts.csswg.org/css-cascade-5/#layer-block 92 | #[derive(Debug, Clone, PartialEq)] 93 | pub struct LayerBlockRule<'i> { 94 | pub name: Option>, 95 | pub rules: CssRuleList<'i>, 96 | pub loc: Location, 97 | } 98 | 99 | impl<'i> ToCss for LayerBlockRule<'i> { 100 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 101 | where 102 | W: std::fmt::Write, 103 | { 104 | dest.add_mapping(self.loc); 105 | dest.write_str("@layer")?; 106 | if let Some(name) = &self.name { 107 | dest.write_char(' ')?; 108 | name.to_css(dest)?; 109 | } 110 | 111 | dest.whitespace()?; 112 | dest.write_char('{')?; 113 | dest.indent(); 114 | dest.newline()?; 115 | self.rules.to_css(dest)?; 116 | dest.dedent(); 117 | dest.newline()?; 118 | dest.write_char('}') 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /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::{Parse, ToCss}; 6 | use cssparser::*; 7 | 8 | /// A generic value that represents a value for four sides of a box, 9 | /// e.g. border-width, margin, padding, etc. 10 | /// 11 | /// When serialized, as few components as possible are written when 12 | /// there are duplicate values. 13 | #[derive(Clone, Debug, PartialEq, Eq)] 14 | pub struct Rect( 15 | /// The top component. 16 | pub T, 17 | /// The right component. 18 | pub T, 19 | /// The bottom component. 20 | pub T, 21 | /// The left component. 22 | pub T, 23 | ); 24 | 25 | impl Rect { 26 | /// Returns a new `Rect` value. 27 | pub fn new(first: T, second: T, third: T, fourth: T) -> Self { 28 | Rect(first, second, third, fourth) 29 | } 30 | } 31 | 32 | impl Default for Rect { 33 | fn default() -> Rect { 34 | Rect::all(T::default()) 35 | } 36 | } 37 | 38 | impl Rect 39 | where 40 | T: Clone, 41 | { 42 | /// Returns a rect with all the values equal to `v`. 43 | pub fn all(v: T) -> Self { 44 | Rect::new(v.clone(), v.clone(), v.clone(), v) 45 | } 46 | 47 | /// Parses a new `Rect` value with the given parse function. 48 | pub fn parse_with<'i, 't, Parse>( 49 | input: &mut Parser<'i, 't>, 50 | parse: Parse, 51 | ) -> Result>> 52 | where 53 | Parse: Fn(&mut Parser<'i, 't>) -> Result>>, 54 | { 55 | let first = parse(input)?; 56 | let second = if let Ok(second) = input.try_parse(|i| parse(i)) { 57 | second 58 | } else { 59 | // 60 | return Ok(Self::new(first.clone(), first.clone(), first.clone(), first)); 61 | }; 62 | let third = if let Ok(third) = input.try_parse(|i| parse(i)) { 63 | third 64 | } else { 65 | // 66 | return Ok(Self::new(first.clone(), second.clone(), first, second)); 67 | }; 68 | let fourth = if let Ok(fourth) = input.try_parse(|i| parse(i)) { 69 | fourth 70 | } else { 71 | // 72 | return Ok(Self::new(first, second.clone(), third, second)); 73 | }; 74 | // 75 | Ok(Self::new(first, second, third, fourth)) 76 | } 77 | } 78 | 79 | impl<'i, T> Parse<'i> for Rect 80 | where 81 | T: Clone + PartialEq + Parse<'i>, 82 | { 83 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 84 | Self::parse_with(input, T::parse) 85 | } 86 | } 87 | 88 | impl ToCss for Rect 89 | where 90 | T: PartialEq + ToCss, 91 | { 92 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 93 | where 94 | W: std::fmt::Write, 95 | { 96 | self.0.to_css(dest)?; 97 | let same_vertical = self.0 == self.2; 98 | let same_horizontal = self.1 == self.3; 99 | if same_vertical && same_horizontal && self.0 == self.1 { 100 | return Ok(()); 101 | } 102 | dest.write_str(" ")?; 103 | self.1.to_css(dest)?; 104 | if same_vertical && same_horizontal { 105 | return Ok(()); 106 | } 107 | dest.write_str(" ")?; 108 | self.2.to_css(dest)?; 109 | if same_horizontal { 110 | return Ok(()); 111 | } 112 | dest.write_str(" ")?; 113 | self.3.to_css(dest) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/values/url.rs: -------------------------------------------------------------------------------- 1 | //! CSS url() values. 2 | 3 | use crate::dependencies::{Dependency, 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 | use cssparser::*; 9 | 10 | /// A CSS [url()](https://www.w3.org/TR/css-values-4/#urls) value and its source location. 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub struct Url<'i> { 13 | /// The url string. 14 | pub url: CowArcStr<'i>, 15 | /// The location where the `url()` was seen in the CSS source file. 16 | pub loc: SourceLocation, 17 | } 18 | 19 | impl<'i> Parse<'i> for Url<'i> { 20 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 21 | let loc = input.current_source_location(); 22 | let url = input.expect_url()?.into(); 23 | Ok(Url { url, loc }) 24 | } 25 | } 26 | 27 | impl<'i> ToCss for Url<'i> { 28 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 29 | where 30 | W: std::fmt::Write, 31 | { 32 | let dep = if dest.dependencies.is_some() { 33 | Some(UrlDependency::new(self, dest.filename())) 34 | } else { 35 | None 36 | }; 37 | 38 | // If adding dependencies, always write url() with quotes so that the placeholder can 39 | // be replaced without escaping more easily. Quotes may be removed later during minification. 40 | if let Some(dep) = dep { 41 | dest.write_str("url(")?; 42 | serialize_string(&dep.placeholder, dest)?; 43 | dest.write_char(')')?; 44 | 45 | if let Some(dependencies) = &mut dest.dependencies { 46 | dependencies.push(Dependency::Url(dep)) 47 | } 48 | 49 | return Ok(()); 50 | } 51 | 52 | use cssparser::ToCss; 53 | if dest.minify { 54 | let mut buf = String::new(); 55 | Token::UnquotedUrl(CowRcStr::from(self.url.as_ref())).to_css(&mut buf)?; 56 | 57 | // If the unquoted url is longer than it would be quoted (e.g. `url("...")`) 58 | // then serialize as a string and choose the shorter version. 59 | if buf.len() > self.url.len() + 7 { 60 | let mut buf2 = String::new(); 61 | serialize_string(&self.url, &mut buf2)?; 62 | if buf2.len() + 5 < buf.len() { 63 | dest.write_str("url(")?; 64 | dest.write_str(&buf2)?; 65 | return dest.write_char(')'); 66 | } 67 | } 68 | 69 | dest.write_str(&buf)?; 70 | } else { 71 | Token::UnquotedUrl(CowRcStr::from(self.url.as_ref())).to_css(dest)?; 72 | } 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | impl<'i> Url<'i> { 79 | /// Returns whether the URL is absolute, and not relative. 80 | pub fn is_absolute(&self) -> bool { 81 | let url = self.url.as_ref(); 82 | 83 | // Quick checks. If the url starts with '.', it is relative. 84 | if url.starts_with('.') { 85 | return false; 86 | } 87 | 88 | // If the url starts with '/' it is absolute. 89 | if url.starts_with('/') { 90 | return true; 91 | } 92 | 93 | // Otherwise, we might have a scheme. These must start with an ascii alpha character. 94 | // https://url.spec.whatwg.org/#scheme-start-state 95 | if !url.starts_with(|c| matches!(c, 'a'..='z' | 'A'..='Z')) { 96 | return false; 97 | } 98 | 99 | // https://url.spec.whatwg.org/#scheme-state 100 | for b in url.as_bytes() { 101 | let c = *b as char; 102 | match c { 103 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.' => {} 104 | ':' => return true, 105 | _ => break, 106 | } 107 | } 108 | 109 | false 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scripts/build-npm.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const pkg = require('../package.json'); 3 | 4 | const dir = `${__dirname}/..`; 5 | const triples = [ 6 | 'x86_64-apple-darwin', 7 | 'x86_64-unknown-linux-gnu', 8 | 'x86_64-pc-windows-msvc', 9 | 'aarch64-apple-darwin', 10 | 'aarch64-unknown-linux-gnu', 11 | 'armv7-unknown-linux-gnueabihf', 12 | 'aarch64-unknown-linux-musl', 13 | 'x86_64-unknown-linux-musl' 14 | ]; 15 | const cpuToNodeArch = { 16 | x86_64: 'x64', 17 | aarch64: 'arm64', 18 | i686: 'ia32', 19 | armv7: 'arm', 20 | }; 21 | const sysToNodePlatform = { 22 | linux: 'linux', 23 | freebsd: 'freebsd', 24 | darwin: 'darwin', 25 | windows: 'win32', 26 | }; 27 | 28 | let optionalDependencies = {}; 29 | let cliOptionalDependencies = {}; 30 | 31 | try { 32 | fs.mkdirSync(dir + '/npm'); 33 | } catch (err) {} 34 | 35 | for (let triple of triples) { 36 | let [cpu, , os, abi] = triple.split('-'); 37 | cpu = cpuToNodeArch[cpu] || cpu; 38 | os = sysToNodePlatform[os] || os; 39 | 40 | let t = `${os}-${cpu}`; 41 | if (abi) { 42 | t += '-' + abi; 43 | } 44 | 45 | buildNode(triple, cpu, os, t); 46 | buildCLI(triple, cpu, os, t); 47 | } 48 | 49 | pkg.optionalDependencies = optionalDependencies; 50 | fs.writeFileSync(`${dir}/package.json`, JSON.stringify(pkg, false, 2) + '\n'); 51 | 52 | let cliPkg = {...pkg}; 53 | cliPkg.name += '-cli'; 54 | cliPkg.bin = { 55 | 'parcel-css': 'parcel_css' 56 | }; 57 | delete cliPkg.main; 58 | delete cliPkg.napi; 59 | delete cliPkg.devDependencies; 60 | delete cliPkg.targets; 61 | delete cliPkg.types; 62 | cliPkg.files = ['parcel_css', 'postinstall.js']; 63 | cliPkg.optionalDependencies = cliOptionalDependencies; 64 | cliPkg.scripts = { 65 | postinstall: 'node postinstall.js' 66 | }; 67 | 68 | fs.writeFileSync(`${dir}/cli/package.json`, JSON.stringify(cliPkg, false, 2) + '\n'); 69 | fs.copyFileSync(`${dir}/README.md`, `${dir}/cli/README.md`); 70 | 71 | function buildNode(triple, cpu, os, t) { 72 | let name = `parcel-css.${t}.node`; 73 | 74 | let pkg2 = {...pkg}; 75 | pkg2.name += '-' + t; 76 | pkg2.os = [os]; 77 | pkg2.cpu = [cpu]; 78 | pkg2.main = name; 79 | pkg2.files = [name]; 80 | delete pkg2.napi; 81 | delete pkg2.devDependencies; 82 | delete pkg2.dependencies; 83 | delete pkg2.optionalDependencies; 84 | delete pkg2.targets; 85 | delete pkg2.scripts; 86 | delete pkg2.types; 87 | 88 | optionalDependencies[pkg2.name] = pkg.version; 89 | 90 | try { 91 | fs.mkdirSync(dir + '/npm/node-' + t); 92 | } catch (err) {} 93 | fs.writeFileSync(`${dir}/npm/node-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n'); 94 | fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${name}`, `${dir}/npm/node-${t}/${name}`); 95 | fs.writeFileSync(`${dir}/npm/node-${t}/README.md`, `This is the ${triple} build of @parcel/css. See https://github.com/parcel-bundler/parcel-css for details.`); 96 | } 97 | 98 | function buildCLI(triple, cpu, os, t) { 99 | let binary = os === 'win32' ? 'parcel_css.exe' : 'parcel_css'; 100 | let pkg2 = {...pkg}; 101 | pkg2.name += '-cli-' + t; 102 | pkg2.os = [os]; 103 | pkg2.cpu = [cpu]; 104 | pkg2.files = [binary]; 105 | delete pkg2.main; 106 | delete pkg2.napi; 107 | delete pkg2.devDependencies; 108 | delete pkg2.dependencies; 109 | delete pkg2.optionalDependencies; 110 | delete pkg2.targets; 111 | delete pkg2.scripts; 112 | delete pkg2.types; 113 | 114 | cliOptionalDependencies[pkg2.name] = pkg.version; 115 | 116 | try { 117 | fs.mkdirSync(dir + '/npm/cli-' + t); 118 | } catch (err) {} 119 | fs.writeFileSync(`${dir}/npm/cli-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n'); 120 | fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${binary}`, `${dir}/npm/cli-${t}/${binary}`); 121 | fs.chmodSync(`${dir}/npm/cli-${t}/${binary}`, 0o755); // Ensure execute bit is set. 122 | fs.writeFileSync(`${dir}/npm/cli-${t}/README.md`, `This is the ${triple} build of @parcel/css-cli. See https://github.com/parcel-bundler/parcel-css for details.`); 123 | } 124 | -------------------------------------------------------------------------------- /src/properties/position.rs: -------------------------------------------------------------------------------- 1 | //! The CSS position property. 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::targets::Browsers; 10 | use crate::traits::{Parse, PropertyHandler, ToCss}; 11 | use crate::vendor_prefix::VendorPrefix; 12 | use cssparser::*; 13 | 14 | /// A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property. 15 | #[derive(Debug, Clone, PartialEq)] 16 | pub enum Position { 17 | /// The box is laid in the document flow. 18 | Static, 19 | /// The box is laid out in the document flow and offset from the resulting position. 20 | Relative, 21 | /// The box is taken out of document flow and positioned in reference to its relative ancestor. 22 | Absolute, 23 | /// Similar to relative but adjusted according to the ancestor scrollable element. 24 | Sticky(VendorPrefix), 25 | /// The box is taken out of the document flow and positioned in reference to the page viewport. 26 | Fixed, 27 | } 28 | 29 | impl<'i> Parse<'i> for Position { 30 | fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { 31 | let location = input.current_source_location(); 32 | let ident = input.expect_ident()?; 33 | match_ignore_ascii_case! { &*ident, 34 | "static" => Ok(Position::Static), 35 | "relative" => Ok(Position::Relative), 36 | "absolute" => Ok(Position::Absolute), 37 | "fixed" => Ok(Position::Fixed), 38 | "sticky" => Ok(Position::Sticky(VendorPrefix::None)), 39 | "-webkit-sticky" => Ok(Position::Sticky(VendorPrefix::WebKit)), 40 | _ => Err(location.new_unexpected_token_error( 41 | cssparser::Token::Ident(ident.clone()) 42 | )) 43 | } 44 | } 45 | } 46 | 47 | impl ToCss for Position { 48 | fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> 49 | where 50 | W: std::fmt::Write, 51 | { 52 | match self { 53 | Position::Static => dest.write_str("static"), 54 | Position::Relative => dest.write_str("relative"), 55 | Position::Absolute => dest.write_str("absolute"), 56 | Position::Fixed => dest.write_str("fixed"), 57 | Position::Sticky(prefix) => { 58 | prefix.to_css(dest)?; 59 | dest.write_str("sticky") 60 | } 61 | } 62 | } 63 | } 64 | 65 | #[derive(Default)] 66 | pub(crate) struct PositionHandler { 67 | targets: Option, 68 | position: Option, 69 | } 70 | 71 | impl PositionHandler { 72 | pub fn new(targets: Option) -> PositionHandler { 73 | PositionHandler { 74 | targets, 75 | ..PositionHandler::default() 76 | } 77 | } 78 | } 79 | 80 | impl<'i> PropertyHandler<'i> for PositionHandler { 81 | fn handle_property( 82 | &mut self, 83 | property: &Property<'i>, 84 | _: &mut DeclarationList<'i>, 85 | _: &mut PropertyHandlerContext<'i>, 86 | ) -> bool { 87 | if let Property::Position(position) = property { 88 | if let (Some(Position::Sticky(cur)), Position::Sticky(new)) = (&mut self.position, position) { 89 | *cur |= *new; 90 | } else { 91 | self.position = Some(position.clone()); 92 | } 93 | 94 | return true; 95 | } 96 | 97 | false 98 | } 99 | 100 | fn finalize(&mut self, dest: &mut DeclarationList, _: &mut PropertyHandlerContext<'i>) { 101 | if self.position.is_none() { 102 | return; 103 | } 104 | 105 | if let Some(position) = std::mem::take(&mut self.position) { 106 | match position { 107 | Position::Sticky(mut prefix) => { 108 | if prefix.contains(VendorPrefix::None) { 109 | if let Some(targets) = self.targets { 110 | prefix = Feature::Sticky.prefixes_for(targets) 111 | } 112 | } 113 | 114 | if prefix.contains(VendorPrefix::WebKit) { 115 | dest.push(Property::Position(Position::Sticky(VendorPrefix::WebKit))) 116 | } 117 | 118 | if prefix.contains(VendorPrefix::None) { 119 | dest.push(Property::Position(Position::Sticky(VendorPrefix::None))) 120 | } 121 | } 122 | _ => dest.push(Property::Position(position)), 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/css_modules.rs: -------------------------------------------------------------------------------- 1 | use crate::error::PrinterErrorKind; 2 | use crate::properties::css_modules::{Composes, ComposesFrom}; 3 | use crate::selector::Selectors; 4 | use data_encoding::{Encoding, Specification}; 5 | use lazy_static::lazy_static; 6 | use parcel_selectors::SelectorList; 7 | use serde::Serialize; 8 | use std::collections::hash_map::DefaultHasher; 9 | use std::collections::HashMap; 10 | use std::hash::{Hash, Hasher}; 11 | 12 | #[derive(PartialEq, Debug, Clone, Serialize)] 13 | #[serde(tag = "type", rename_all = "lowercase")] 14 | pub enum CssModuleReference { 15 | Local { name: String }, 16 | Global { name: String }, 17 | Dependency { name: String, specifier: String }, 18 | } 19 | 20 | #[derive(PartialEq, Debug, Clone, Serialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct CssModuleExport { 23 | pub name: String, 24 | pub composes: Vec, 25 | pub is_referenced: bool, 26 | } 27 | 28 | pub type CssModuleExports = HashMap; 29 | 30 | lazy_static! { 31 | static ref ENCODER: Encoding = { 32 | let mut spec = Specification::new(); 33 | spec 34 | .symbols 35 | .push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-"); 36 | spec.encoding().unwrap() 37 | }; 38 | } 39 | 40 | pub(crate) struct CssModule<'a> { 41 | pub hash: &'a str, 42 | pub exports: &'a mut CssModuleExports, 43 | } 44 | 45 | impl<'a> CssModule<'a> { 46 | pub fn add_local(&mut self, exported: &str, local: &str) { 47 | self.exports.entry(exported.into()).or_insert_with(|| CssModuleExport { 48 | name: get_hashed_name(self.hash, local), 49 | composes: vec![], 50 | is_referenced: false, 51 | }); 52 | } 53 | 54 | pub fn reference(&mut self, name: &str) { 55 | match self.exports.entry(name.into()) { 56 | std::collections::hash_map::Entry::Occupied(mut entry) => { 57 | entry.get_mut().is_referenced = true; 58 | } 59 | std::collections::hash_map::Entry::Vacant(entry) => { 60 | entry.insert(CssModuleExport { 61 | name: get_hashed_name(self.hash, name), 62 | composes: vec![], 63 | is_referenced: true, 64 | }); 65 | } 66 | } 67 | } 68 | 69 | pub fn handle_composes( 70 | &mut self, 71 | selectors: &SelectorList, 72 | composes: &Composes, 73 | ) -> Result<(), PrinterErrorKind> { 74 | for sel in &selectors.0 { 75 | if sel.len() == 1 { 76 | match sel.iter_raw_match_order().next().unwrap() { 77 | parcel_selectors::parser::Component::Class(ref id) => { 78 | for name in &composes.names { 79 | let reference = match &composes.from { 80 | None => CssModuleReference::Local { 81 | name: get_hashed_name(self.hash, name.0.as_ref()), 82 | }, 83 | Some(ComposesFrom::Global) => CssModuleReference::Global { 84 | name: name.0.as_ref().into(), 85 | }, 86 | Some(ComposesFrom::File(file)) => CssModuleReference::Dependency { 87 | name: name.0.to_string(), 88 | specifier: file.to_string(), 89 | }, 90 | }; 91 | 92 | let export = self.exports.get_mut(&id.0.as_ref().to_owned()).unwrap(); 93 | if !export.composes.contains(&reference) { 94 | export.composes.push(reference); 95 | } 96 | } 97 | continue; 98 | } 99 | _ => {} 100 | } 101 | } 102 | 103 | // The composes property can only be used within a simple class selector. 104 | return Err(PrinterErrorKind::InvalidComposesSelector); 105 | } 106 | 107 | Ok(()) 108 | } 109 | } 110 | 111 | fn get_hashed_name(hash: &str, name: &str) -> String { 112 | // Hash must come first so that CSS grid identifiers work. 113 | // This is because grid lines may have an implicit -start or -end appended. 114 | format!("{}_{}", hash, name) 115 | } 116 | 117 | pub(crate) fn hash(s: &str) -> String { 118 | let mut hasher = DefaultHasher::new(); 119 | s.hash(&mut hasher); 120 | let hash = hasher.finish() as u32; 121 | 122 | let hash = ENCODER.encode(&hash.to_le_bytes()); 123 | if matches!(hash.as_bytes()[0], b'0'..=b'9') { 124 | format!("_{}", hash) 125 | } else { 126 | hash 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/values/time.rs: -------------------------------------------------------------------------------- 1 | //! CSS time values. 2 | 3 | use super::calc::Calc; 4 | use super::number::CSSNumber; 5 | use crate::error::{ParserError, PrinterError}; 6 | use crate::printer::Printer; 7 | use crate::traits::{Parse, ToCss}; 8 | use cssparser::*; 9 | 10 | /// A CSS [`
74 |

Parcel CSS Playground

75 |
76 | 77 | 78 |
79 |
80 |
81 |

Options

82 | 83 | 84 | 85 |

Draft syntax

86 | 87 | 88 |

Targets

89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 104 | 108 |
109 | 133 | 134 | 135 | 136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 css from './native.js'; 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