Skip to content

Commit 71c6d38

Browse files
committed
Add option to replace pseudo classes with class names for polyfills
1 parent b9aab14 commit 71c6d38

File tree

6 files changed

+149
-15
lines changed

6 files changed

+149
-15
lines changed

node/index.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,27 @@ export interface TransformOptions {
2121
* are replaced with hashed placeholders that can be replaced with the final
2222
* urls later (after bundling). Dependencies are returned as part of the result.
2323
*/
24-
analyzeDependencies?: boolean
24+
analyzeDependencies?: boolean,
25+
/**
26+
* Replaces user action pseudo classes with class names that can be applied from JavaScript.
27+
* This is useful for polyfills, for example.
28+
*/
29+
pseudoClasses?: PseudoClasses
2530
}
2631

2732
export interface Drafts {
2833
/** Whether to enable CSS nesting. */
2934
nesting?: boolean
3035
}
3136

37+
export interface PseudoClasses {
38+
hover?: string,
39+
active?: string,
40+
focus?: string,
41+
focusVisible?: string,
42+
focusWithin?: string
43+
}
44+
3245
export interface TransformResult {
3346
/** The transformed code. */
3447
code: Buffer,

node/src/lib.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
44

55
use serde::{Serialize, Deserialize};
6-
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions};
6+
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions, PseudoClasses};
77
use parcel_css::targets::Browsers;
88
use parcel_css::css_modules::CssModuleExports;
99
use parcel_css::dependencies::Dependency;
@@ -102,7 +102,7 @@ fn init(mut exports: JsObject) -> napi::Result<()> {
102102

103103
// ---------------------------------------------
104104

105-
#[derive(Serialize, Debug, Deserialize)]
105+
#[derive(Debug, Deserialize)]
106106
#[serde(rename_all = "camelCase")]
107107
struct Config {
108108
pub filename: String,
@@ -113,7 +113,30 @@ struct Config {
113113
pub source_map: Option<bool>,
114114
pub drafts: Option<Drafts>,
115115
pub css_modules: Option<bool>,
116-
pub analyze_dependencies: Option<bool>
116+
pub analyze_dependencies: Option<bool>,
117+
pub pseudo_classes: Option<OwnedPseudoClasses>
118+
}
119+
120+
#[derive(Debug, Deserialize)]
121+
#[serde(rename_all = "camelCase")]
122+
struct OwnedPseudoClasses {
123+
pub hover: Option<String>,
124+
pub active: Option<String>,
125+
pub focus: Option<String>,
126+
pub focus_visible: Option<String>,
127+
pub focus_within: Option<String>
128+
}
129+
130+
impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
131+
fn into(self) -> PseudoClasses<'a> {
132+
PseudoClasses {
133+
hover: self.hover.as_deref(),
134+
active: self.active.as_deref(),
135+
focus: self.focus.as_deref(),
136+
focus_visible: self.focus_visible.as_deref(),
137+
focus_within: self.focus_within.as_deref()
138+
}
139+
}
117140
}
118141

119142
#[derive(Serialize, Debug, Deserialize, Default)]
@@ -135,7 +158,8 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
135158
minify: config.minify.unwrap_or(false),
136159
source_map: config.source_map.unwrap_or(false),
137160
targets: config.targets,
138-
analyze_dependencies: config.analyze_dependencies.unwrap_or(false)
161+
analyze_dependencies: config.analyze_dependencies.unwrap_or(false),
162+
pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into())
139163
})?;
140164

141165
let map = if let Some(mut source_map) = res.source_map {
@@ -189,7 +213,8 @@ fn compile_attr<'i>(code: &'i str, config: &AttrConfig) -> Result<AttrResult, Co
189213
minify: config.minify.unwrap_or(false),
190214
source_map: false,
191215
targets: config.targets,
192-
analyze_dependencies: config.analyze_dependencies.unwrap_or(false)
216+
analyze_dependencies: config.analyze_dependencies.unwrap_or(false),
217+
pseudo_classes: None
193218
})?;
194219
Ok(AttrResult {
195220
code: res.code.into_bytes(),

src/lib.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7834,4 +7834,69 @@ mod tests {
78347834
"foo" => "foo_EgL3uq"
78357835
});
78367836
}
7837+
7838+
#[test]
7839+
fn test_pseudo_replacement() {
7840+
let source = r#"
7841+
.foo:hover {
7842+
color: red;
7843+
}
7844+
7845+
.foo:active {
7846+
color: yellow;
7847+
}
7848+
7849+
.foo:focus-visible {
7850+
color: purple;
7851+
}
7852+
"#;
7853+
7854+
let expected = indoc! { r#"
7855+
.foo.is-hovered {
7856+
color: red;
7857+
}
7858+
7859+
.foo.is-active {
7860+
color: #ff0;
7861+
}
7862+
7863+
.foo.focus-visible {
7864+
color: purple;
7865+
}
7866+
"#};
7867+
7868+
let stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions::default()).unwrap();
7869+
let res = stylesheet.to_css(PrinterOptions {
7870+
pseudo_classes: Some(PseudoClasses {
7871+
hover: Some("is-hovered"),
7872+
active: Some("is-active"),
7873+
focus_visible: Some("focus-visible"),
7874+
..PseudoClasses::default()
7875+
}),
7876+
..PrinterOptions::default()
7877+
}).unwrap();
7878+
assert_eq!(res.code, expected);
7879+
7880+
let source = r#"
7881+
.foo:hover {
7882+
color: red;
7883+
}
7884+
"#;
7885+
7886+
let expected = indoc! { r#"
7887+
.foo_EgL3uq.is-hovered_EgL3uq {
7888+
color: red;
7889+
}
7890+
"#};
7891+
7892+
let stylesheet = StyleSheet::parse("test.css".into(), source, ParserOptions { css_modules: true, ..ParserOptions::default() }).unwrap();
7893+
let res = stylesheet.to_css(PrinterOptions {
7894+
pseudo_classes: Some(PseudoClasses {
7895+
hover: Some("is-hovered"),
7896+
..PseudoClasses::default()
7897+
}),
7898+
..PrinterOptions::default()
7899+
}).unwrap();
7900+
assert_eq!(res.code, expected);
7901+
}
78377902
}

src/printer.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ use crate::targets::Browsers;
66
use crate::css_modules::CssModule;
77
use crate::dependencies::Dependency;
88

9+
#[derive(Default, Debug)]
10+
pub struct PseudoClasses<'a> {
11+
pub hover: Option<&'a str>,
12+
pub active: Option<&'a str>,
13+
pub focus: Option<&'a str>,
14+
pub focus_visible: Option<&'a str>,
15+
pub focus_within: Option<&'a str>
16+
}
17+
918
pub(crate) struct Printer<'a, W> {
1019
pub filename: &'a str,
1120
dest: &'a mut W,
@@ -20,7 +29,8 @@ pub(crate) struct Printer<'a, W> {
2029
pub vendor_prefix: VendorPrefix,
2130
pub in_calc: bool,
2231
pub css_module: Option<CssModule<'a>>,
23-
pub dependencies: Option<&'a mut Vec<Dependency>>
32+
pub dependencies: Option<&'a mut Vec<Dependency>>,
33+
pub pseudo_classes: Option<PseudoClasses<'a>>
2434
}
2535

2636
impl<'a, W: Write + Sized> Printer<'a, W> {
@@ -43,7 +53,8 @@ impl<'a, W: Write + Sized> Printer<'a, W> {
4353
vendor_prefix: VendorPrefix::empty(),
4454
in_calc: false,
4555
css_module: None,
46-
dependencies: None
56+
dependencies: None,
57+
pseudo_classes: None
4758
}
4859
}
4960

src/selector.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,30 @@ impl ToCssWithContext for PseudoClass {
356356
}};
357357
}
358358

359+
macro_rules! pseudo {
360+
($key: ident, $s: literal) => {{
361+
let class = if let Some(pseudo_classes) = &dest.pseudo_classes {
362+
pseudo_classes.$key
363+
} else {
364+
None
365+
};
366+
367+
if let Some(class) = class {
368+
dest.write_char('.')?;
369+
dest.write_ident(class)
370+
} else {
371+
dest.write_str($s)
372+
}
373+
}};
374+
}
375+
359376
match &self {
360377
// https://drafts.csswg.org/selectors-4/#useraction-pseudos
361-
Hover => dest.write_str(":hover"),
362-
Active => dest.write_str(":active"),
363-
Focus => dest.write_str(":focus"),
364-
FocusVisible => dest.write_str(":focus-visible"),
365-
FocusWithin => dest.write_str(":focus-within"),
378+
Hover => pseudo!(hover, ":hover"),
379+
Active => pseudo!(active, ":active"),
380+
Focus => pseudo!(focus, ":focus"),
381+
FocusVisible => pseudo!(focus_visible, ":focus-visible"),
382+
FocusWithin => pseudo!(focus_within, ":focus-within"),
366383

367384
// https://drafts.csswg.org/selectors-4/#time-pseudos
368385
Current => dest.write_str(":current"),

src/stylesheet.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::collections::HashMap;
1111
use crate::dependencies::Dependency;
1212

1313
pub use crate::parser::ParserOptions;
14+
pub use crate::printer::PseudoClasses;
1415

1516
pub struct StyleSheet {
1617
pub filename: String,
@@ -19,11 +20,12 @@ pub struct StyleSheet {
1920
}
2021

2122
#[derive(Default)]
22-
pub struct PrinterOptions {
23+
pub struct PrinterOptions<'a> {
2324
pub minify: bool,
2425
pub source_map: bool,
2526
pub targets: Option<Browsers>,
26-
pub analyze_dependencies: bool
27+
pub analyze_dependencies: bool,
28+
pub pseudo_classes: Option<PseudoClasses<'a>>
2729
}
2830

2931
pub struct ToCssResult {
@@ -82,6 +84,7 @@ impl StyleSheet {
8284
};
8385

8486
printer.dependencies = dependencies.as_mut();
87+
printer.pseudo_classes = options.pseudo_classes;
8588

8689
if self.options.css_modules {
8790
let h = hash(&self.filename);

0 commit comments

Comments
 (0)