Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for prefixed parent selector #2861

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

denizsokullu
Copy link

Formal Proposal based on Issue#1425
Previous Proposal

Copy link
Contributor

@nex3 nex3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for diving into this, Deniz!

Comment on lines 87 to 89
###Proposal #1:

The current [Style Rules Semantics](https://github.com/sass/sass/blob/master/spec/style-rules.md#semantics) allows parent selectors anywhere in a `selector` and does not explain why it currently errors for prefixed parent selectors. If the prefixed parent selector is supported, the semantics section for Style Rules would not need to be changed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section doesn't say anything about where a parent selector can be because we think of that as a syntactic restriction. Sass won't even parse foo&, so by the time a style rule is being evaluated we know any parent selectors are in valid locations.

That means that this proposal needs to update the syntax as well as the semantics. This should be clear enough to know whether constructs like foo&bar, &foo&, foo&& and so on are valid syntax.

The semantics should also be made more explicit, similarly to what you've done in Proposal 2. The spec is written lazily, so you'll probably need to do some work to specify the existing behavior as well as the new behavior you're adding.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I'm one of @denizsokullu's coworkers and worked on this proposal with him.

We looked at https://github.com/sass/sass/blob/master/CONTRIBUTING.md#proposal as well as the accepted proposals at https://github.com/sass/sass/tree/master/accepted and were under the impression that either a Syntax or Semantics section was required, not both. Are you saying that we must provide both in order for this proposal to be accepted?

Also when you say "update the syntax" do you mean that there is an existing syntax specification somewhere? We weren't able to find one which is why we were proposing that the existing spec supports this syntax.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We looked at https://github.com/sass/sass/blob/master/CONTRIBUTING.md#proposal as well as the accepted proposals at https://github.com/sass/sass/tree/master/accepted and were under the impression that either a Syntax or Semantics section was required, not both. Are you saying that we must provide both in order for this proposal to be accepted?

It depends on what the proposal is changing:

  • For proposals like Plain CSS min() and max(), the only thing that's changing is how the parser converts the input text into a syntax tree. Once the syntax tree is constructed, the existing language behavior will process it appropriately. In that case, all that's necessary is a "Syntax" section.

  • For proposals like Configuring Modules Through Imports, the existing syntax tree remains the same and only the way it's executed changes. In that case, only a "Semantics" section is necessary.

  • But most proposals, especially those that add new language features like @content Arguments or this one, describe both new ways to parse the input file and how to handle the newly-parsed structures. In cases like that, both a "Syntax" and "Semantics" section are necessary, respectively.

Also when you say "update the syntax" do you mean that there is an existing syntax specification somewhere? We weren't able to find one which is why we were proposing that the existing spec supports this syntax.

There's kind of a theoretical existing syntax specification. The spec is added to lazily, so many existing features aren't fully written out until they're first updated by a proposal. For selector parsing, the current syntax would probably look something like this:

CompoundSelector       ::= InitialSimpleSelector
                         | InitialSimpleSelector? TrailingSimpleSelector+
InitialSimpleSelector  ::= <type-selector> | '&' <wq-name>?
TrailingSimpleSelector ::= <subclass-selector> | <pseudo-element-selector>

...where the angle-bracket productions are defined by Selectors Level 4.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's kind of a theoretical existing syntax specification. The spec is added to lazily, so many existing features aren't fully written out until they're first updated by a proposal.

If the syntax section only needs to specify how parent selectors are handled we can add that to our proposal. I've never written anything in the grammar language used by web standards, and only rarely read it, so we may need some help fixing mistakes.

The semantics should also be made more explicit, similarly to what you've done in Proposal 2. The spec is written lazily, so you'll probably need to do some work to specify the existing behavior as well as the new behavior you're adding.

The current Style Rules Semantics say the following about parent selectors:

If selector contains one or more parent selectors, replace them with the current style rule's selector and set selector to the result.

Do we need to describe the changes to just this line, or do we need to first expand out the entire style rules semantics section? And if it is the former, what is missing from the change we suggested in our second proposal?

Keep in mind that like many of the other people who want this feature, we are coming from another CSS compiler that supports prefixed parent selectors. In our case, the system is PHPSass, which supports the Sass syntax from 6 years ago plus some extra stuff like the prefixed parent selector. As a result our knowledge of Sass's current syntax and semantics is limited to what we've used and what is already documented. Plus some of what we think we already know about Sass may be inaccurate because of the extra syntax that PHPSass supported.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the syntax section only needs to specify how parent selectors are handled we can add that to our proposal. I've never written anything in the grammar language used by web standards, and only rarely read it, so we may need some help fixing mistakes.

I'm always happy to help 😄.

Do we need to describe the changes to just this line, or do we need to first expand out the entire style rules semantics section? And if it is the former, what is missing from the change we suggested in our second proposal?

The section should be expanded, at least to some degree. You don't need to fully describe the process of resolving nesting, but the process of converting a compound selector that contains parent selectors into a selector list that does not should be explained.

@denizsokullu denizsokullu requested a review from nex3 June 2, 2020 15:55
Copy link
Contributor

@nex3 nex3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I forgot to mention last time: please include a "Summary" section that describes in human terms the new feature that's being added.

Comment on lines 84 to 90
SelectorList ::= ComplexSelectorList#
ComplexSelectorList ::= ComplexSelector#
CompoundSelectorList ::= CompoundSelector#
SimpleSelectorList ::= SimpleSelector#
RelativeSelectorList ::= RelativeSelector#
ComplexSelector ::= CompoundSelector [ <combinator>? CompoundSelector ]*
RelativeSelector ::= <combinator>? ComplexSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, I think you can omit these productions and rely on updating the CompoundSelector production to implicitly propagate to the relevant places.

Comment on lines 94 to 96
SimpleSelector ::= ParentSelector? <simple-selector>
| <simple-selector> ParentSelector
| ParentSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we actually want to allow ParentSelector in the simple selector production. This production is only used when simple selectors are on their own in a document, in which cases including a parent selector is generally invalid (since you can't use &.foo, it doesn't make sense to use foo& either).

Comment on lines 91 to 93
CompoundSelector ::= ParentSelector? <compound-selector>
| <compound-selector> ParentSelector
| ParentSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing a parent selector to appear before or after any compound selector isn't quite correct, because it allows cases like &* or [foo]& which we want to forbid. That's why, in my example description of the existing syntax, I effectively made '&' a sibling of <type-selector>: it should only appear at the beginning of a compound selector, optionally with a prefix and/or suffix.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to expand ParentSelector to include their own prefixs or suffixes like an InterpolatedIdentifier? e.g.:

ParentSelector ::= (<ident-token> | '-'? Interpolation)? '&' (Name | Interpolation)*
TypeSelector ::= InterpolatedIdentifer | ParentSelector

CompoundSelector ::= TypeSelector? <subclass-selector>*

Copy link

@jquense jquense Jun 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess that doesn't cover the && case :/ Just realizing that & in sass is more like a variable that can be used anywhere. So this would work fine I think? since the grammar rules are only about where it can used bare in a selector.

Comment on lines 100 to 118
Note that it is not necessary to support the following syntax:
```
.custom-effect {
p&:focus {
// styles for <p> when focused
}
}
```

Because the same effect can be achieved with one extra level of nesting:
```
.custom-effect {
p& {
&:focus {
// styles for <p> when focused
}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be pretty surprising if we supported foo& and &.bar but not foo&.bar. Similarly, I think foo&bar should be supported. Making sure features compose in ways that users expect is important for making the language feel consistent and comprehensible.

Comment on lines 133 to 134
* If multiple `ParentSelector` appear in the a single `SimpleSelector` or `CompoundSelector`,
throw an error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already forbidden by the syntax, so checking it here shouldn't be necessary.

Comment on lines 136 to 137
* Otherwise, replace each `ParentSelector` with the current style rule's
selector and set `selector` to the result.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this still needs some more detail to describe how to handle ParentSelectors with prefixes and suffixes. For example, it should be detailed enough to explain that * { foo& { /* ... */ } } is an error and how .bar { foo& { /* ... */ } } works differently than bar { foo& { /* ... */ } }.

@denizsokullu denizsokullu requested a review from nex3 June 3, 2020 14:39
@nex3
Copy link
Contributor

nex3 commented Jun 17, 2020

Just checking in to say I haven't forgotten about this—I ended up not working most of last week, and I've been catching up with the backlog, but I intend to loop back here before the end of the week.

Comment on lines +113 to +125
CompoundSelector ::= [ ParentSelector
| <type-selector>? <subclass-selector>* PseudoSelectors*
| TypeSelectorWithParent <subclass-selector>* PseudoSelectors*
| <type-selector>? SubclassSelectorsWithParent PseudoSelectors*
| <type-selector>? <subclass-selector>*
PseudoSelectorsWithParent ]!
TypeSelectorWithParent ::= ParentSelector <wq-name> | <type-selector> ParentSelector
SubclassSelectorsWithParent ::= ParentSelector <subclass-selector>+
| <subclass-selector>+ ParentSelector <subclass-selector>*
PseudoSelectorsWithParent ::= ParentSelector PseudoSelectors+
| PseudoSelectors+ ParentSelector PseudoSelectors*
PseudoSelectors ::= <pseudo-element-selector> <pseudo-class-selector>*
ParentSelector ::= '&'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're making this more complex than it needs to be. @jquense has the right idea: you want to make ParentSelector a sibling of TypeSelector, and you want to make the ParentSelector production itself allow an InterpolatedIdentifier as a prefix and/or suffix. So something like this:

CompoundSelector       ::= InitialSimpleSelector
                         | InitialSimpleSelector? TrailingSimpleSelector+
InitialSimpleSelector  ::= TypeSelector | InterpolatedIdentifier? '&' InterpolatedIdentifier?
TrailingSimpleSelector ::= SubclassSelector | PseudoElementSelector

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the complexity came from the desire to put the parent selector anywhere inside the compound selector while only allowing there to be one parent selector per compound selector.

Because the definition of <compound-selector> allows <pseudo-class-selector> to appear after <pseudo-element-selector> (the [ <pseudo-element-selector> <pseudo-class-selector>* ]* part in https://www.w3.org/TR/selectors-4/#typedef-compound-selector), I assumed the following should be supported:

::after {
    ::before& {
        ...
    }
    ::before&:hover {
        ...
    }
    ::before:hover& {
        ...
    }
}

Unless I am misunderstanding your suggested syntax (always possible), I don't think it allows for something like ::before&:hover even though the standard allows for ::before:hover in a compound selector.

Note that if we allow more than one parent selector per compound selector the syntax would get a lot simpler but it seems weird to support that because it would just allow for duplication in selectors.

Copy link

@arkonan arkonan Jun 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After playing a little with https://www.sassmeister.com/ I noticed that Sass's idea a <compound-selector> is actually much more liberal than https://www.w3.org/TR/selectors-4/#typedef-compound-selector.

For example the following compiles even though the spec says that only <pseudo-class-selector> is allowed to follow a <pseudo-element-selector>:

::before.bar {
    ...
}
::before#foo {
  color: white;
}

If the actual Sass definition of <compound-selector> is more like the following then I completely understand why you think our proposed syntax is excessively complex:

CompoundSelector ::= [ <type-selector>? SubclassSelector* ]!
SubclassSelector ::= <subclass-selector> | <pseudo-element-selector>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the definition of <compound-selector> allows <pseudo-class-selector> to appear after <pseudo-element-selector> (the [ <pseudo-element-selector> <pseudo-class-selector>* ]* part in https://www.w3.org/TR/selectors-4/#typedef-compound-selector), I assumed the following should be supported:

::after {
    ::before& {
        ...
    }
    ::before&:hover {
        ...
    }
    ::before:hover& {
        ...
    }
}

My thought was that this would not be allowed, and that a parent selector would (still) only be able to appear at the beginning of a compound selector with an identifier prefix and/or suffix. I can see the argument for allowing it to appear anywhere, but it seems like a much less pressing use-case than combining a parent selector with a type selector. I'd prefer to specify it more narrowly for now and expand it later if we see a compelling use-case.

After playing a little with https://www.sassmeister.com/ I noticed that Sass's idea a <compound-selector> is actually much more liberal than https://www.w3.org/TR/selectors-4/#typedef-compound-selector.

For example the following compiles even though the spec says that only <pseudo-class-selector> is allowed to follow a <pseudo-element-selector>:

::before.bar {
    ...
}
::before#foo {
  color: white;
}

If the actual Sass definition of <compound-selector> is more like the following then I completely understand why you think our proposed syntax is excessively complex:

CompoundSelector ::= [ <type-selector>? SubclassSelector* ]!
SubclassSelector ::= <subclass-selector> | <pseudo-element-selector>

That's right; this is a result of Sass's general principle to be flexible in the CSS it allows in order to be as forwards-compatible as possible. CSS today forbids peudo-elements anywhere but the end of a selector, but who's to say they won't add a new pseudo-element later that works differently?

Copy link

@arkonan arkonan Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our code base actually uses the parent selector following an attribute selector because of the ElementQuery plugin. This JavaScript plugin uses attribute selectors to generate a media-query like interface for DOM elements which don't extend the full width or height of the viewport.

The following is a simplified example that hides an inner thumbnail image by default but allows it to appear when the wrapper gets wider. (Note that the hidden case is the default because ElementQuery runs after DOM load so we want the visible DOM modifications to only appear in the less common, wider case.)

.wrapper {
    /* ... */

    .thumbnail {
        display: none;

        [min-width~="17em"]& {
            display: block;
        }
    }

    /* ... */
}

We are also using parent selectors after class selectors (e.g. .my-class&) and pseudo-selectors (e.g. :not(.my-class)&).

I feel like it will be much less surprising to developers if the parent selector is allowed anywhere in a compound selector rather than just before or after an identifier token. It seems especially weird to me that Sass would support div& but not support :not(div)&.

Fortunately because of Sass's more liberal interpretation of the location of <pseudo-element-selector>the syntax isn't crazy like our first proposal. Something like this should be sufficient:

CompoundSelector ::= [ ParentSelector? <ident-token>? TrailingSelector*
                     | <type-selector>? TrailingSelector* ParentSelector? TrailingSelector* ]!
TrailingSelector ::= <subclass-selector> | <pseudo-element-selector>
ParentSelector   ::= '&'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll bring this use-case up at our design meeting next week and get the team's opinions on it.

Comment on lines +142 to +143
* If the result of the replacement(s) is not a syntactically valid
[complex selector][], throw an error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively requires re-parsing the entire selector, which is relatively expensive. Writing it this way also means that a reader has to manually think through all the cases that might be invalid, making it easier to misinterpret the spec.

I think we can much more explicitly and efficiently detect whether the parent is a valid replacement by just enumerating the types of selectors that are not valid when used with prefixes and/or suffixes.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it helps I think the logic is something like:

For each Parent Selector Complex selector, a parent prefix must be compatible with the first CompoundSelector's first component and the suffix must be compatible with the last Compound selector's last component.

  • A suffix is compatible with ID, Type, Class, or Pseudo without arguments
  • prefix is compatible with type selectors or as a type selector itself (I think).

Sorry for jumping in here unannounced, i've been writing a little Sass-Like preprocessor in postcss and PEG.js so have been thinking through the grammar of Sass more than one might normally, and thought it may be helpful to share!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contributions are totally welcome!

I think a prefixed parent selector should be compatible with both type selectors (as long as they aren't universal) and what the spec refers to as "subclass selcetors". .foo { a& { ... } } and foo { bar-& { ... } } use a prefixed parent selector for different purposes, but I think both purposes are valid.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

I think we'll wait for your input on the [ <pseudo-element-selector> <pseudo-class-selector>* ]* part of the compound selector specification before starting this though. The number of allowed combinations needing to be listed out depends on whether or not we support things like ::before&:hover.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from the conversation it sounds like we want to support things like this:

.foo {
    &-bar {
        ...
    }
}

to output

.foo-bar { ... }

(Note that this appears to already work at https://www.sassmeister.com/)

We could also support this:

foo {
    .&-bar {
        ...
    }
}

also to output

.foo-bar { ... }

And this:

.foo {
    div&-bar {
        ...
    }
}

could turn into this:

div.foo-bar { ... }

I can see these being useful for certain CSS methodologies, but they will add some complexity to the semantics for determining if the current style rule is compatible with the parent selector's location. For example in the first case the string with the parent selector in it changes from what appears to be a <ident-token> (-bar) to a <class-selector> (.foo-bar) after the substitution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not support .&-bar but we should support div&-bar. The former, as you say, adds a tremendous amount of syntactic complexity for very little gain. The latter is a natural consequence of allowing both prefixes and suffixes on parent selectors (although I would personally consider it questionable style).

Copy link

@arkonan arkonan Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.&-bar should be disallowed by the syntax I proposed above because the parent selector is in the middle of a class selector.

If we do allow things like div&-bar the semantics get a lot simpler because they mostly just have to check for things like making sure a type selector doesn't end up with two namespaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants