-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Integrate value & selector parsing into PostCSS core. #754
Comments
I think that integrating selector and value parsers into Core would make a very positive difference. So many plugins either do or absolutely should use those parsers. Those that should but don't probably would use the parsers, and therefore improve their plugins, if they were integrated into Core. (Maybe they don't know the parsers exist, or maybe they don't know to trust the parsers, I don't know.) In the tree I think we should make sure to use CSS verbiage instead of JS or more programming-language-y verbiage. Words that people will find when looking at MDN, the spec, and articles about CSS. For example, |
@davidtheclark Yeah, this language confused me at first too, which is why I am not sure about how we should begin to represent complex expressions in a simple way. So my understanding of an '@'.charCodeAt(0); Now, we may not use the exact same terminology for CSS grammar, but we need to think about appropriate structures for values. All of these types should be represented; border: 1px solid red; /* space separated list of values */
font-family: Helvetica, Arial, sans-serif; /* comma separated list of values */
background-image: url(unicorn.jpg); /* function */
background: red url(unicorn.jpg) repeat-y; /* space separated list of values with a function */
content: 'foobar'; /* string literals */ Arguably each of these values would be expressions if we were using JS terminology. But if we are not to use expression, then what do we use? value? But each of the members inside the expression are values. So this can be confusing if we don't get it right, any suggestions are welcome. 😃 |
At https://www.w3.org/TR/css3-values/, https://drafts.csswg.org/css-syntax-3/#component-value, and https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax I see regular use of "component value". Might we use I'm not suggesting that we're going to find a super transparent word to regular CSS users, or that the term Expression is not clear enough when explained; just that I'd hope we could use words that are derived from the CSS spec rather than other languages. |
I wasn't aware that this document existed; of course, we should aim to use words derived from the specification whenever possible. 👍 |
@thejameskyle’s suggestion seems like the only way to go forward for things like precss, too. Count me as a yes to this. |
I just want to note that going down this path will also limit the flexibility people will have in custom syntax (which seems to be very common in PostCSS plugin). For example, if you parse selectors, then invalid character sequences will cause parse errors that don't exist today. It is possible to write parser plugins but that's much harder to support than code transforms. You can see the specification for the Babel AST here. Obviously this does not map to CSS, but you might draw some inspiration from it, in particular the expression and literal types/sub-types. If you want to read some discussions about AST design, you can read through the ESTree Issues. Another thing you might want to consider with this change is switching to a more formal visitor pattern. For example, this is what Babel's visitors look like: path.traverse({
Identifier(path) {
// ...
}
}); This pattern can be better optimized (for example Babel merges all the top level plugin visitors into one giant visitor so it only has to traverse the tree once), and in my experience leads to cleaner and less stateful transforms. A good example of this is the transforms I wrote for both Babel and JSCodeShift to transform pure react component classes into functions: Babel, JSCodeShift. These do almost the same exact thing and it was easy to add more functionality to the Babel transform. Let me know if there's anything I can do to help this along. |
@thejameskyle I'm not sure that I follow on the custom syntax thing; it is true that PostCSS plugins do define their own syntax sometimes, but I think that it is done within the limits of the CSS grammar as it is today. In cases where it isn't, for example single line comments, we have custom parsers that provide extensions to the default parser to support these use cases. Perhaps you could follow up with an example that illustrates your problem? 😄 Similarly, https://github.com/postcss/postcss-selector-parser does parse selectors but makes no assumptions about for example pseudo selectors that it doesn't understand; e.g. you can easily define some made up function and it will still be parsed correctly: a:unicorn(stoic) {
} Is parsed the same as: span:not(.stoic) {
} |
Say someone wanted to do interpolation of a variable into a selector: .foo-#{$bar} { ... } If you are parsing through the selector I imagine you will have all sorts of conflicts here. |
OK sure; but right now, PostCSS doesn't support that kind of expression. It would require a custom parser to do this. 😄 |
BTW, we still need to have old API. So
|
@thejameskyle the good part of CSS is that we can parse any unknown syntaxes as |
Why? What if some plugin authors choose to update
I can't see how keeping this API around provides any benefit over more appropriate node parsing. It should be either/or, not both. |
@ben-eb any call of |
The difficulty that you're going to have here is that a lot of these transforms are actually interpreting the code
Resolving this requires plugins handing off responsibility and not running in any particular sequence. For example, if you have a plugin that resolves Now, a couple things need to happen to ensure that we don't have a ridiculous number of tree traversals or easy to break plugins: Visitors that can be run on any part of the tree at any time If I'm plugin Visitors need to be able to visit nodes on "exit" How does Instead, When nodes change, they should trigger re-traversal Imagine two plugins, one that resolves a When the Because of this, there needs to be an API around manipulating nodes instead of mutating them directly If people are setting What Babel does is wrap all nodes with node "paths" (please read this). When I call Sorry for the wall of dense text. The good news is that compilers are a solved problem in computer science (people have been doing pretty much the exact same thing for almost 20 years), the bad news is that there's a ton of stuff you need to know (I have three books on compilers each around 1000 pages long). TL;DR: Parsing a more detailed AST is only half of the story, in order to resolve the original problem of function resolution (along with tons of other problems) the transformation API needs to change dramatically in order to solve the problem in a more maintainable way. |
@thejameskyle thanks for this big post. It is really useful |
@thejameskyle @ai Agreed, looks like we have a lot to learn. |
That was really great. I love how @thejameskyle describes the problem so clearly, and then reassures us that it was solved years ago. Whew. Except now we gotta learn stuff. :) |
So we should implement #296 before this issue |
I just wanted to interject with my opinion. As I see it, this type of API could be optional and invoked on-demand, saving us the need to alter the structure of the AST from how it is today. I imagine something akin to the following: ...
var selectorAST = postcss.selector.parse(rule.selector);
// do something with selectorAST
rule.selector = postcss.selector.stringify(selectorAST)
... This would address @thejameskyle's concern about custom syntax, as well as save us the need to parse each and every node in the tree. |
I think we must parse each and every node. Currently we don't do this, which is what I find to be brittle and means that plugins must be run in an exact order; this is not a good developer experience. |
I don't follow why you think parsing nodes (and by this I mean parsing a rule's selector value into an AST, for instance) is a necessity. I agree that the plugin order thing makes for a poor developer experience, but, as I see it, that issue is not rooted in the lack of an AST for node values, but rather that certain transformations need to have taken place before the plugin can successfully execute in accordance to what the user would expect. This is a big drawback in postcss' design. What's more, some issues can't be remedied by simply changing plugin order, such as when plugins depend on each-other's state. However, both these issues would be addressed by executing the nodes one-by-one against the visitors, then re-executing a node against the visitors once it's been changed. Let me demonstrate using a couple of examples: Example 1 someprop: foo(bar(baz())); Assume that So.. When the stylesheet is first processed, the visitors would run against the node. someprop: foo(quux); Since Example 2 Assuming postcss-simple-vars and postcss-conditionals, you cannot redefine a variable inside a condition because the whole AST is processed once by the first plugin, then once by the second plugin. // define $a and $b
...
@if $b == 0 {
$b: 1;
}
result: calc($a / $b);
... In the above example it would always redefine Now, if we had visitors, postcss would walk through the node tree one node at a time and run it through the visitors. First postcss-simple-vars would define Assuming the opposite plugin order, I imagine postcss-conditionals not to throw a syntax error upon seeing PS. I don't think your AST-for-node-values idea would work with postcss-conditionals, which is why making it optional, still allowing for this kind of syntax, would be a good idea. PPS. Since the above example wouldn't error due to the assumption that the Sorry for the wall of text. |
It's a necessity because in order to facilitate two ways of writing plugins (querying a node's value tree or querying a node's stringified value), PostCSS needs to maintain a string representation of a node's value (for instance) as well as a node tree. So that whichever one is set will update the other, in order to satisfy a plugin's expectations. That seems really inefficient to me as PostCSS will have to do this for every plugin. The rest of your post I believe is how Babel works; calling a I'm not sure about errors. Perhaps they should be handled after exiting the |
And the previous proposed solution to that doesn't satisfy that need? var ast = postcss.selector.parse(rule.selector); |
@andyjansson I think you aren't understanding the problem completely. Say we have this selector: fn1(fn2(fn1(foo))) { ... } Then I have plugins for Solution 1: Run in order If we ran Solution 2: Repeatedly try to resolve Here we keep running Solution 3: Have a tree structure that can be folded Here we parse the entire AST and do a depth-first traversal, resolving dependencies upwards. This solves the issue of ordering and the issue of plugins trying to figure out if they can't resolve a value because of a syntax error or because of another plugin that needs to run. Solution 3 is also the fastest and easiest to reason about solution, which is why it is used in every modern compiler. What you suggest simply wouldn't work. |
@thejameskyle what you just described sounds exactly what I've been talking about...?
I addressed this concern at the bottom of my post
Yes, I understand that, but this also means that we cannot use custom syntax. In case of, say, postcss-functions, it does resolve depth first.
When I said syntax error earlier, I didn't mean a CSS syntax error. I was referring to a syntax error pertaining to the custom syntax of the if statement.
I've laid out my reasoning and for the examples I've presented, it does work. Care to provide an example where it wouldn't? I'm not trying to be dismissive of what you're saying, it's just that there seem to be some information missing here. |
Well.. it does and doesn't. For argument's sake, let's have 20 plugins in the same PostCSS instance that use So for example with a unified API we can iterate through all comment nodes whether they are in selectors or values, and then remove them: {
comment: {
enter: path => path.remove()
}
} This is much harder to do at the moment because a developer has to negotiate three different parsers. |
Yep, I totally get that. I'm totally for an AST provided that it doesn't hamper the ability to write values which doesn't follow conventional CSS value syntax (such as the aforementioned postcss-conditionals plugin). I do not see that it would be possible if the AST is the mandatory form of mutation. If however there are two properties, one for string manipulation and the other for AST manipulation, one compiling its value to the other, I could see it working. It would however necitate that the AST property can be null (since you can write values which cannot be parsed). |
If this is all about being able to parse custom syntax there are ways of making that happen without abandoning a detailed AST. CSS is a pretty straightforward syntax to parse, and there are well documented ways of extending parsers to understand more syntax. But as I believe @ai mentioned before, most of the custom syntax people want doesn't conflict with the syntax that would be parsed by PostCSS. |
FYI: I'm currently working on a parser for CSS Selectors with support for Level 4 Selectors as defined here. It's not yet complete (traversing and manipulating the AST is missing), but if you are interested in integrating the parser into PostCSS I'd be happy to chat (and design/adjust the API according to suggestions). Otherwise I'd be happy if we could have an active discussion about the AST for Selectors, so I can steer my parser into the same direction (and maybe use it as a drop-in replacement at some point). |
Yeap, let's talk in PostCSS gitter after May 12 (sorry, conference). What do you think about this steps:
Also, what will be the main difference with postcss-selector-parser? |
Well one major advantage so far is, that it supports plugins for the parser. This allows correct parsing of |
@clentfort awesome! @ben-eb what do you think? |
Do we want to go down this road with PostCSS? We could make PostCSS only parse 'legal' grammars, but that is slower than "loosely" parsing as we are doing right now. If anyone wanted some custom syntax it would be harder to implement than it is currently. For my use case I don't care about the semantics of tokens inside pseudo-selectors, which is why it works like it does. I don't think it can be that big of a deal given that it's been an open issue since last July. I guess tagging things 'help wanted' doesn't seem to make much of difference. |
As I said, my selectors parses nested not-callst, even though strictly speaking it is not a valid CSS selector. I really can't see any disadvantage by parsing the parameters of a I wonder about your concerns about the speed, I already mentioned that I benchmarked my parser against Regarding custom grammars: Well this is as much a problem with |
Is this still something that is being considered / worked on? |
Yeap, this issue is in process inside "common parse for Sasd, CSS and Less" project. But it will not be solved soon. |
Thanks for the answer. Is there any way to help with the effort? Would love to see this feature land in postcss. |
@hokkaido not right now. My current plans:
So it is hard to make it faster. What is your current problem? Maybe I can suggest faster solution? |
Close for #1145 |
So, one of my main concerns about writing cssnano version 4 is that I am trying to glue together lots of different parsers, each with different APIs, and I find myself wanting a much richer AST than what PostCSS currently provides. I feel that as per @thejameskyle's suggestion, we should look at parsing selectors and values;
https://github.com/thejameskyle/postcss-function-resolution
This means that we need to extend
Declaration
andRule
to provide a more detailed AST. I don't exactly know how this should look yet, so any suggestions are welcome. But James' rough draft looks good;Furthermore, I'd like to drop
node.raws.value.value
andnode.raws.value.raw
from the AST completely and force plugin authors to provide support for comments or not in their plugins; the current behaviour is that PostCSS will hide comments automatically, which can cause problems with things like userstyles. This would be much easier when we have a unified AST that covers values and selectors as plugin authors can just ignore comments, and instead operate on relevant values.I am concerned that this is a big change and is not something that I will consider lightly, so I'm looking for feedback. It makes writing complex plugins like cssnano & stylelint easier at the expense of smaller plugins, but I feel like this allows us to do some really sophisticated transforms/analysis in the future.
This issue supercedes #410, #235 & #178.
The text was updated successfully, but these errors were encountered: