Skip to content

Commit

Permalink
Add support for non-standard >>> and /deep/ selector combinators behi…
Browse files Browse the repository at this point in the history
…nd a flag

#495
  • Loading branch information
devongovett committed May 24, 2023
1 parent eaf1f32 commit 99e1a2e
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 6 deletions.
7 changes: 7 additions & 0 deletions node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface TransformOptions<C extends CustomAtRules> {
targets?: Targets,
/** Whether to enable various draft syntax. */
drafts?: Drafts,
/** Whether to enable various non-standard syntax. */
nonStandard?: NonStandard,
/** Whether to compile this file as a CSS module. */
cssModules?: boolean | CSSModulesConfig,
/**
Expand Down Expand Up @@ -259,6 +261,11 @@ export interface Drafts {
customMedia?: boolean
}

export interface NonStandard {
/** Whether to enable the non-standard >>> and /deep/ selector combinators used by Angular and Vue. */
deepSelectorCombinator?: boolean
}

export interface PseudoClasses {
hover?: string,
active?: string,
Expand Down
21 changes: 21 additions & 0 deletions node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ struct Config {
pub source_map: Option<bool>,
pub input_source_map: Option<String>,
pub drafts: Option<Drafts>,
pub non_standard: Option<NonStandard>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
Expand Down Expand Up @@ -540,6 +541,7 @@ struct BundleConfig {
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub drafts: Option<Drafts>,
pub non_standard: Option<NonStandard>,
pub css_modules: Option<CssModulesOption>,
pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
pub pseudo_classes: Option<OwnedPseudoClasses>,
Expand Down Expand Up @@ -579,12 +581,20 @@ struct Drafts {
custom_media: bool,
}

#[derive(Serialize, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct NonStandard {
#[serde(default)]
deep_selector_combinator: bool,
}

fn compile<'i>(
code: &'i str,
config: &Config,
visitor: &mut Option<JsVisitor>,
) -> Result<TransformResult<'i>, CompileError<'i, std::io::Error>> {
let drafts = config.drafts.as_ref();
let non_standard = config.non_standard.as_ref();
let warnings = Some(Arc::new(RwLock::new(Vec::new())));

let filename = config.filename.clone().unwrap_or_default();
Expand All @@ -602,6 +612,11 @@ fn compile<'i>(
let mut flags = ParserFlags::empty();
flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting));
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
flags.set(
ParserFlags::DEEP_SELECTOR_COMBINATOR,
matches!(non_standard, Some(v) if v.deep_selector_combinator),
);

let mut stylesheet = StyleSheet::parse_with(
&code,
ParserOptions {
Expand Down Expand Up @@ -714,9 +729,15 @@ fn compile_bundle<

let res = {
let drafts = config.drafts.as_ref();
let non_standard = config.non_standard.as_ref();
let mut flags = ParserFlags::empty();
flags.set(ParserFlags::NESTING, matches!(drafts, Some(d) if d.nesting));
flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
flags.set(
ParserFlags::DEEP_SELECTOR_COMBINATOR,
matches!(non_standard, Some(v) if v.deep_selector_combinator),
);

let parser_options = ParserOptions {
flags,
css_modules: if let Some(css_modules) = &config.css_modules {
Expand Down
2 changes: 1 addition & 1 deletion node/test/composeVisitors.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test('different types', () => {
])
});

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

test('simple matching types', () => {
Expand Down
16 changes: 16 additions & 0 deletions node/test/transform.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { transform } from '../index.mjs';
import { test } from 'uvu';
import * as assert from 'uvu/assert';

test('can enable non-standard syntax', () => {
let res = transform({
filename: 'test.css',
code: Buffer.from('.foo >>> .bar { color: red }'),
nonStandard: {
deepSelectorCombinator: true
},
minify: true
});

assert.equal(res.code.toString(), '.foo>>>.bar{color:red}');
});
2 changes: 1 addition & 1 deletion node/test/visitor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ test('px to rem', () => {
}
});

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

test('custom units', () => {
Expand Down
4 changes: 3 additions & 1 deletion selectors/matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ where
{
match combinator {
Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(),
Combinator::Child | Combinator::Descendant => {
Combinator::Child | Combinator::Descendant | Combinator::Deep | Combinator::DeepDescendant => {
match element.parent_element() {
Some(e) => return Some(e),
None => {}
Expand Down Expand Up @@ -479,6 +479,8 @@ where
SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant
}
Combinator::Child
| Combinator::Deep
| Combinator::DeepDescendant
| Combinator::Descendant
| Combinator::SlotAssignment
| Combinator::Part
Expand Down
44 changes: 42 additions & 2 deletions selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ pub trait Parser<'i> {
fn is_nesting_allowed(&self) -> bool {
false
}

fn deep_combinator_enabled(&self) -> bool {
false
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -1128,6 +1132,15 @@ pub enum Combinator {
/// Another combinator used for `::part()`, which represents the jump from
/// the part to the containing shadow host.
Part,

/// Non-standard Vue >>> combinator.
/// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors
DeepDescendant,
/// Non-standard /deep/ combinator.
/// Appeared in early versions of the css-scoping-1 specification:
/// https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#deep-combinator
/// And still supported as an alias for >>> by Vue.
Deep,
}

impl Combinator {
Expand Down Expand Up @@ -1770,6 +1783,8 @@ impl ToCss for Combinator {
Combinator::Descendant => dest.write_str(" "),
Combinator::NextSibling => dest.write_str(" + "),
Combinator::LaterSibling => dest.write_str(" ~ "),
Combinator::DeepDescendant => dest.write_str(" >>> "),
Combinator::Deep => dest.write_str(" /deep/ "),
Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
}
}
Expand Down Expand Up @@ -2020,7 +2035,18 @@ where
Err(_e) => break 'outer_loop,
Ok(&Token::WhiteSpace(_)) => any_whitespace = true,
Ok(&Token::Delim('>')) => {
combinator = Combinator::Child;
if parser.deep_combinator_enabled()
&& input
.try_parse(|input| {
input.expect_delim('>')?;
input.expect_delim('>')
})
.is_ok()
{
combinator = Combinator::DeepDescendant;
} else {
combinator = Combinator::Child;
}
break;
}
Ok(&Token::Delim('+')) => {
Expand All @@ -2031,6 +2057,20 @@ where
combinator = Combinator::LaterSibling;
break;
}
Ok(&Token::Delim('/')) if parser.deep_combinator_enabled() => {
if input
.try_parse(|input| {
input.expect_ident_matching("deep")?;
input.expect_delim('/')
})
.is_ok()
{
combinator = Combinator::Deep;
break;
} else {
break 'outer_loop;
}
}
Ok(_) => {
input.reset(&before_this_token);
if any_whitespace {
Expand Down Expand Up @@ -2605,7 +2645,7 @@ where
}
SimpleSelectorParseResult::PseudoElement(p) => {
if !p.is_unknown() {
state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
builder.push_combinator(Combinator::PseudoElement);
}
if !p.accepts_state_pseudo_classes() {
Expand Down
30 changes: 29 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ mod tests {
}

fn minify_test(source: &str, expected: &str) {
let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
minify_test_with_options(source, expected, ParserOptions::default())
}

fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) {
let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap();
stylesheet.minify(MinifyOptions::default()).unwrap();
let res = stylesheet
.to_css(PrinterOptions {
Expand Down Expand Up @@ -6682,6 +6686,30 @@ mod tests {
".foo ::unknown:only-child {width: 20px}",
".foo ::unknown:only-child{width:20px}",
);

let deep_options = ParserOptions {
flags: ParserFlags::DEEP_SELECTOR_COMBINATOR,
..ParserOptions::default()
};

error_test(
".foo >>> .bar {width: 20px}",
ParserError::SelectorError(SelectorError::DanglingCombinator),
);
error_test(
".foo /deep/ .bar {width: 20px}",
ParserError::SelectorError(SelectorError::DanglingCombinator),
);
minify_test_with_options(
".foo >>> .bar {width: 20px}",
".foo>>>.bar{width:20px}",
deep_options.clone(),
);
minify_test_with_options(
".foo /deep/ .bar {width: 20px}",
".foo /deep/ .bar{width:20px}",
deep_options.clone(),
);
}

#[test]
Expand Down
2 changes: 2 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ bitflags! {
const NESTING = 1 << 0;
/// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax.
const CUSTOM_MEDIA = 1 << 1;
/// Whether to enable the non-standard >>> and /deep/ selector combinators used by Vue and Angular.
const DEEP_SELECTOR_COMBINATOR = 1 << 2;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o,
fn is_nesting_allowed(&self) -> bool {
self.is_nesting_allowed
}

fn deep_combinator_enabled(&self) -> bool {
self.options.flags.contains(ParserFlags::DEEP_SELECTOR_COMBINATOR)
}
}

enum_property! {
Expand Down Expand Up @@ -1195,6 +1199,12 @@ impl ToCss for Combinator {
Combinator::Descendant => dest.write_str(" "),
Combinator::NextSibling => dest.delim('+', true),
Combinator::LaterSibling => dest.delim('~', true),
Combinator::Deep => dest.write_str(" /deep/ "),
Combinator::DeepDescendant => {
dest.whitespace()?;
dest.write_str(">>>")?;
dest.whitespace()
}
Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
}
}
Expand Down

0 comments on commit 99e1a2e

Please sign in to comment.