Skip to content

Commit 99e1a2e

Browse files
committed
Add support for non-standard >>> and /deep/ selector combinators behind a flag
parcel-bundler#495
1 parent eaf1f32 commit 99e1a2e

File tree

10 files changed

+132
-6
lines changed

10 files changed

+132
-6
lines changed

node/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface TransformOptions<C extends CustomAtRules> {
2525
targets?: Targets,
2626
/** Whether to enable various draft syntax. */
2727
drafts?: Drafts,
28+
/** Whether to enable various non-standard syntax. */
29+
nonStandard?: NonStandard,
2830
/** Whether to compile this file as a CSS module. */
2931
cssModules?: boolean | CSSModulesConfig,
3032
/**
@@ -259,6 +261,11 @@ export interface Drafts {
259261
customMedia?: boolean
260262
}
261263

264+
export interface NonStandard {
265+
/** Whether to enable the non-standard >>> and /deep/ selector combinators used by Angular and Vue. */
266+
deepSelectorCombinator?: boolean
267+
}
268+
262269
export interface PseudoClasses {
263270
hover?: string,
264271
active?: string,

node/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ struct Config {
496496
pub source_map: Option<bool>,
497497
pub input_source_map: Option<String>,
498498
pub drafts: Option<Drafts>,
499+
pub non_standard: Option<NonStandard>,
499500
pub css_modules: Option<CssModulesOption>,
500501
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
501502
pub pseudo_classes: Option<OwnedPseudoClasses>,
@@ -540,6 +541,7 @@ struct BundleConfig {
540541
pub minify: Option<bool>,
541542
pub source_map: Option<bool>,
542543
pub drafts: Option<Drafts>,
544+
pub non_standard: Option<NonStandard>,
543545
pub css_modules: Option<CssModulesOption>,
544546
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
545547
pub pseudo_classes: Option<OwnedPseudoClasses>,
@@ -579,12 +581,20 @@ struct Drafts {
579581
custom_media: bool,
580582
}
581583

584+
#[derive(Serialize, Debug, Deserialize, Default)]
585+
#[serde(rename_all = "camelCase")]
586+
struct NonStandard {
587+
#[serde(default)]
588+
deep_selector_combinator: bool,
589+
}
590+
582591
fn compile<'i>(
583592
code: &'i str,
584593
config: &Config,
585594
visitor: &mut Option<JsVisitor>,
586595
) -> Result<TransformResult<'i>, CompileError<'i, std::io::Error>> {
587596
let drafts = config.drafts.as_ref();
597+
let non_standard = config.non_standard.as_ref();
588598
let warnings = Some(Arc::new(RwLock::new(Vec::new())));
589599

590600
let filename = config.filename.clone().unwrap_or_default();
@@ -602,6 +612,11 @@ fn compile<'i>(
602612
let mut flags = ParserFlags::empty();
603613
flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting));
604614
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
615+
flags.set(
616+
ParserFlags::DEEP_SELECTOR_COMBINATOR,
617+
matches!(non_standard, Some(v) if v.deep_selector_combinator),
618+
);
619+
605620
let mut stylesheet = StyleSheet::parse_with(
606621
&code,
607622
ParserOptions {
@@ -714,9 +729,15 @@ fn compile_bundle<
714729

715730
let res = {
716731
let drafts = config.drafts.as_ref();
732+
let non_standard = config.non_standard.as_ref();
717733
let mut flags = ParserFlags::empty();
718734
flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting));
719735
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
736+
flags.set(
737+
ParserFlags::DEEP_SELECTOR_COMBINATOR,
738+
matches!(non_standard, Some(v) if v.deep_selector_combinator),
739+
);
740+
720741
let parser_options = ParserOptions {
721742
flags,
722743
css_modules: if let Some(css_modules) = &config.css_modules {

node/test/composeVisitors.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test('different types', () => {
4141
])
4242
});
4343

44-
assert.equal(res.code.toString(), '.foo{width:1rem;color:#0f0}');
44+
assert.equal(res.code.toString(), '.foo{color:#0f0;width:1rem}');
4545
});
4646

4747
test('simple matching types', () => {

node/test/transform.test.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { transform } from '../index.mjs';
2+
import { test } from 'uvu';
3+
import * as assert from 'uvu/assert';
4+
5+
test('can enable non-standard syntax', () => {
6+
let res = transform({
7+
filename: 'test.css',
8+
code: Buffer.from('.foo >>> .bar { color: red }'),
9+
nonStandard: {
10+
deepSelectorCombinator: true
11+
},
12+
minify: true
13+
});
14+
15+
assert.equal(res.code.toString(), '.foo>>>.bar{color:red}');
16+
});

node/test/visitor.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ test('px to rem', () => {
2828
}
2929
});
3030

31-
assert.equal(res.code.toString(), '.foo{width:2rem;height:calc(100vh - 4rem);--custom:calc(var(--foo) + 2rem)}');
31+
assert.equal(res.code.toString(), '.foo{--custom:calc(var(--foo) + 2rem);width:2rem;height:calc(100vh - 4rem)}');
3232
});
3333

3434
test('custom units', () => {

selectors/matching.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ where
398398
{
399399
match combinator {
400400
Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(),
401-
Combinator::Child | Combinator::Descendant => {
401+
Combinator::Child | Combinator::Descendant | Combinator::Deep | Combinator::DeepDescendant => {
402402
match element.parent_element() {
403403
Some(e) => return Some(e),
404404
None => {}
@@ -479,6 +479,8 @@ where
479479
SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant
480480
}
481481
Combinator::Child
482+
| Combinator::Deep
483+
| Combinator::DeepDescendant
482484
| Combinator::Descendant
483485
| Combinator::SlotAssignment
484486
| Combinator::Part

selectors/parser.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ pub trait Parser<'i> {
350350
fn is_nesting_allowed(&self) -> bool {
351351
false
352352
}
353+
354+
fn deep_combinator_enabled(&self) -> bool {
355+
false
356+
}
353357
}
354358

355359
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -1128,6 +1132,15 @@ pub enum Combinator {
11281132
/// Another combinator used for `::part()`, which represents the jump from
11291133
/// the part to the containing shadow host.
11301134
Part,
1135+
1136+
/// Non-standard Vue >>> combinator.
1137+
/// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors
1138+
DeepDescendant,
1139+
/// Non-standard /deep/ combinator.
1140+
/// Appeared in early versions of the css-scoping-1 specification:
1141+
/// https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#deep-combinator
1142+
/// And still supported as an alias for >>> by Vue.
1143+
Deep,
11311144
}
11321145

11331146
impl Combinator {
@@ -1770,6 +1783,8 @@ impl ToCss for Combinator {
17701783
Combinator::Descendant => dest.write_str(" "),
17711784
Combinator::NextSibling => dest.write_str(" + "),
17721785
Combinator::LaterSibling => dest.write_str(" ~ "),
1786+
Combinator::DeepDescendant => dest.write_str(" >>> "),
1787+
Combinator::Deep => dest.write_str(" /deep/ "),
17731788
Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
17741789
}
17751790
}
@@ -2020,7 +2035,18 @@ where
20202035
Err(_e) => break 'outer_loop,
20212036
Ok(&Token::WhiteSpace(_)) => any_whitespace = true,
20222037
Ok(&Token::Delim('>')) => {
2023-
combinator = Combinator::Child;
2038+
if parser.deep_combinator_enabled()
2039+
&& input
2040+
.try_parse(|input| {
2041+
input.expect_delim('>')?;
2042+
input.expect_delim('>')
2043+
})
2044+
.is_ok()
2045+
{
2046+
combinator = Combinator::DeepDescendant;
2047+
} else {
2048+
combinator = Combinator::Child;
2049+
}
20242050
break;
20252051
}
20262052
Ok(&Token::Delim('+')) => {
@@ -2031,6 +2057,20 @@ where
20312057
combinator = Combinator::LaterSibling;
20322058
break;
20332059
}
2060+
Ok(&Token::Delim('/')) if parser.deep_combinator_enabled() => {
2061+
if input
2062+
.try_parse(|input| {
2063+
input.expect_ident_matching("deep")?;
2064+
input.expect_delim('/')
2065+
})
2066+
.is_ok()
2067+
{
2068+
combinator = Combinator::Deep;
2069+
break;
2070+
} else {
2071+
break 'outer_loop;
2072+
}
2073+
}
20342074
Ok(_) => {
20352075
input.reset(&before_this_token);
20362076
if any_whitespace {
@@ -2605,7 +2645,7 @@ where
26052645
}
26062646
SimpleSelectorParseResult::PseudoElement(p) => {
26072647
if !p.is_unknown() {
2608-
state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
2648+
state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
26092649
builder.push_combinator(Combinator::PseudoElement);
26102650
}
26112651
if !p.accepts_state_pseudo_classes() {

src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ mod tests {
7878
}
7979

8080
fn minify_test(source: &str, expected: &str) {
81-
let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
81+
minify_test_with_options(source, expected, ParserOptions::default())
82+
}
83+
84+
fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) {
85+
let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap();
8286
stylesheet.minify(MinifyOptions::default()).unwrap();
8387
let res = stylesheet
8488
.to_css(PrinterOptions {
@@ -6682,6 +6686,30 @@ mod tests {
66826686
".foo ::unknown:only-child {width: 20px}",
66836687
".foo ::unknown:only-child{width:20px}",
66846688
);
6689+
6690+
let deep_options = ParserOptions {
6691+
flags: ParserFlags::DEEP_SELECTOR_COMBINATOR,
6692+
..ParserOptions::default()
6693+
};
6694+
6695+
error_test(
6696+
".foo >>> .bar {width: 20px}",
6697+
ParserError::SelectorError(SelectorError::DanglingCombinator),
6698+
);
6699+
error_test(
6700+
".foo /deep/ .bar {width: 20px}",
6701+
ParserError::SelectorError(SelectorError::DanglingCombinator),
6702+
);
6703+
minify_test_with_options(
6704+
".foo >>> .bar {width: 20px}",
6705+
".foo>>>.bar{width:20px}",
6706+
deep_options.clone(),
6707+
);
6708+
minify_test_with_options(
6709+
".foo /deep/ .bar {width: 20px}",
6710+
".foo /deep/ .bar{width:20px}",
6711+
deep_options.clone(),
6712+
);
66856713
}
66866714

66876715
#[test]

src/parser.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ bitflags! {
4545
const NESTING = 1 << 0;
4646
/// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax.
4747
const CUSTOM_MEDIA = 1 << 1;
48+
/// Whether to enable the non-standard >>> and /deep/ selector combinators used by Vue and Angular.
49+
const DEEP_SELECTOR_COMBINATOR = 1 << 2;
4850
}
4951
}
5052

src/selector.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o,
316316
fn is_nesting_allowed(&self) -> bool {
317317
self.is_nesting_allowed
318318
}
319+
320+
fn deep_combinator_enabled(&self) -> bool {
321+
self.options.flags.contains(ParserFlags::DEEP_SELECTOR_COMBINATOR)
322+
}
319323
}
320324

321325
enum_property! {
@@ -1195,6 +1199,12 @@ impl ToCss for Combinator {
11951199
Combinator::Descendant => dest.write_str(" "),
11961200
Combinator::NextSibling => dest.delim('+', true),
11971201
Combinator::LaterSibling => dest.delim('~', true),
1202+
Combinator::Deep => dest.write_str(" /deep/ "),
1203+
Combinator::DeepDescendant => {
1204+
dest.whitespace()?;
1205+
dest.write_str(">>>")?;
1206+
dest.whitespace()
1207+
}
11981208
Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
11991209
}
12001210
}

0 commit comments

Comments
 (0)