Less.js: Less fails to parse valid Custom Property values (ex: @apply polyfill)

Created on 21 Oct 2015  ·  34Comments  ·  Source: less/less.js

This code breaks:

paper-drawer-panel {
  --paper-drawer-panel-left-drawer: {
    background-color: red;
  };
}

The curly brackets in the property value break the parser.

I'd like LESS to support the @apply syntax, as used by Polymer.
https://www.polymer-project.org/1.0/docs/devguide/styling.html#custom-css-mixins

Also tried:

paper-drawer-panel {
  @ruleset: {
    prop: value;
  };
  --paper-drawer-panel-left-drawer: %(~"%d", @ruleset);
}

It didn't break the parser but delivered:

paper-drawer-panel {
  --paper-drawer-panel-left-drawer: ;
}

which is clearly not a desirable behavior.

bug feature request

Most helpful comment

We have a spec for this; a polyfill exists; Chrome has implemented it; Polymer is already using it in a 1.0 release; and websites are already using Polymer.

I think it's a perfectly reasonable expectation for Less to support the syntax at a bare minimum level.

Less already passes @apply, --foo: bar;, and var(--foo) through. The only thing missing is getting Less to pass through the { ... } in --foo: { property: value; } with the same basic processing given to other css blocks; instead of coming to a hard stop and throwing a parse error.

All 34 comments

I'm afraid non-CSS syntax support is beyond Less scope. So my -1.

Either way notice you can emit whatever code into the output with escaping. E.g.:

xpaper-drawer-panel {
  --paper-drawer-panel-left-drawer: ~"{background-color: red;}";
}

Or in more hackish multiline way:

paper-drawer-panel {
  -:~";--paper-drawer-panel-left-drawer: {";
    background-color: red;
    color: blue;
  -:~";}";
}

In a more complex situations it's also always possible to use @import (inline).


As for attempts like %(~"%d", @ruleset); - No, Less variables are not macros, they are not expanded regardless of context, and specifically for unnamed rulesets the only proper syntax is @ruleset() within a ruleset body and _not_ as a property value.

Well I agree with you that non-standard syntax should generally not be supported.

However this means that LESS in incompatible with Polymer in many cases.

Thanks for the reply anyway. :+1:

Not sure if this makes a difference in your decision @seven-phases-max but there is an open spec for an @apply rule, http://tabatkins.github.io/specs/css-apply-rule/#issue-882293bd.

Also see https://github.com/Polymer/polymer/issues/1373 as it sounds like something Google is going to really push through the process.

And SASS seems to be addressing this https://github.com/sass/sass/issues/1128

@donny-dont Note I do not decide anything (I did not close the proposal) - I'm only a critic.

Well, Tab Atkins generated _tens_ of CSS standard proposals (named "ideas") and while Google guys may push this into their browser even tomorrow, this is still too far from becoming even CSS draft thing (remember CSS vars not entering CSS world for about ten years?).

@seven-phases-max apologies for the implication. I just wanted to make a note that Polymer is pushing the mixin proposal and Chrome is likely to implement it once its shaped up which was something @Jamtis didn't really communicate.

I'm fairly new to using LESS is there any sort of plugin system for the parser in these sorts of scenarios? Something like an experimental flag you can turn on and off?

I'm reopening this to be able to redirect duplicate requests here.

/sub

We have a spec for this; a polyfill exists; Chrome has implemented it; Polymer is already using it in a 1.0 release; and websites are already using Polymer.

I think it's a perfectly reasonable expectation for Less to support the syntax at a bare minimum level.

Less already passes @apply, --foo: bar;, and var(--foo) through. The only thing missing is getting Less to pass through the { ... } in --foo: { property: value; } with the same basic processing given to other css blocks; instead of coming to a hard stop and throwing a parse error.

I think the full spec of CSS Variables should be supported by LESS. Would love for this to happen!

@dantman

We have a spec for this; a polyfill exists; Chrome has implemented it; Polymer is already using it in a 1.0 release; and websites are already using Polymer.

To me, that's still not a very compelling reason. That translates to: Google, Google, and Google are using this. There's been no interest pop up anywhere else, and there are no indications there ever will be.

@donny-dont

I'm fairly new to using LESS is there any sort of plugin system for the parser in these sorts of scenarios? Something like an experimental flag you can turn on and off?

There is a plugin system, but no direct support for changing parsing. So, that would not be a trivial thing to do.

@dantman
We have a spec for this; a polyfill exists; Chrome has implemented it; Polymer is already using it in a 1.0 release; and websites are already using Polymer.

To me, that's still not a very compelling reason. That translates to: Google, Google, and Google are using this. There's been no interest pop up anywhere else, and there are no indications there ever will be.

That conclusion would make sense if we were talking about a more proprietary and unsupported syntax that deviated significantly from normal css; but we're talking about Less generating parse errors when Less already understands nesting and the syntax in question fits perfectly within CSS' bracket matching based parse rules and doesn't break surrounding css, and the other major CSS preprocessor and postprocessor either are working on supporting it or already work.

That conclusion would make sense if we were talking about a more proprietary and unsupported syntax that deviated significantly from normal css; but we're talking about Less generating parse errors when Less already understands nesting and the syntax in question fits perfectly within CSS' bracket matching based parse rules and doesn't break surrounding css, and the other major CSS preprocessor and postprocessor either are working on supporting it or already work.

Not irrelevant. It's pretty similar to Less's detached rulesets. And there's precedent for other "pass-thru" stuff added to Less. So I'm not necessarily against it. Just skeptical if there is value beyond Polymer at the moment.

Official Support for CSS Variables is listed here: http://caniuse.com/#feat=css-variables

Minimum Browser version for default CSS Variable Support

  • Chrome 49
  • Chrome for Android 51
  • Android Browser 51
  • Firefox for Android 47
  • Firefox 31
  • Safari 9.1
  • iOS Safari 9.3
  • Opera 36
  • Opera Mobile 37

Without a polyfill, this equates to roughly, 65% global browser usage.

With multiple CSS Variable Polyfills out there, browsers implementing it and large browser providers pushing for it. It seems like this would be a valuable feature to have.

Official Spec: https://drafts.csswg.org/css-variables/

@stramel That has nothing to do with this issue. CSS Variables are already supported in Less. The request is about @apply, which is not part of that spec.

EDIT: not all CSS Custom Property values are supported, see below.

@matthew-dean Sorry, I misunderstood. Regardless, :+1: for CSS Mixins/Custom Property Sets

@matthew-dean what is the bar for less supporting? If another browser implemented then good to go?

@donny-dont Since Less is a community project, there's no specific bar. But yes, something beyond a single browser would make it a more general-purpose feature.

Just if someone is searching this (as I did): you can put all the Polymer non-standard css in a separate file and import this with the inline keyword in your less file. The less file has to interface between less variables and polymer custom properties. But that is the way Polymer wants it, anyway. Agree you cannot implement every deviation from standards.

Just to revisit this, I think I unfortunately had a narrow view of the scope of the problem.

That is: yes, @apply is just a proposal, BUT REGARDLESS, _this_ is _not_ invalid CSS. Not anymore.

paper-drawer-panel {
  --paper-drawer-panel-left-drawer: {
    background-color: red;
  };
}

My mistake was thinking that @apply was proposing an extension to the Custom Property syntax, but it isn't. The above _IS_ a valid custom property value, and, as such, should be added to Less support. I wish I had taken the time to dig deeper into the spec. (Here: https://www.w3.org/TR/css-variables/#syntax.)

Less has a core goal to support valid CSS, and Custom Property syntax is implemented fully in every browser as of now. The spec is extremely permissive, and may require some big changes in the way property values are parsed. You can put almost anything into it. Just how crazy you can get with values and whether browsers will allow that, I'm not sure. But, the way I read the spec, mostly anything is valid if it's well formed. Meaning, until you encounter a "top-level" semicolon token, you can go nuts, wrapping all sorts of stuff in curly braces or parentheses.

This means that JavaScript libraries like Polymer are increasingly likely to "hack" CSS to place declarative properties / values where they're readable by JS, which was really not possible before this CSS feature.

So 👍 to implementing ASAP.

Sorry to those who tried to point out that this is valid custom property syntax.

I'm marking this both as bug and feature request because a) Less claims to parse custom property values, but fails sometimes (but it's not clear when), but b) the types of values Less needs to handle is beyond the original design of even CSS itself, let alone Less, so it is a new feature.

Another test that can be added to Less tests for custom property parsing. This is perfectly valid CSS as of today.

.test {
  --custom-function: () => { let x = 1; window['NAMESPACE'].doSomething(x); };
}

Essentially, once something is in () [] or {}, you can dump in any characters you want. So, semi-colons would pass in the above example. It requires some recursive parsing until braces get matched and closed (if/when they're opened) before you can test for a closing semi-colon.

I started that work here but not sure yet if this is correct. https://github.com/less/less.js/blob/edge/lib/less/parser/parser.js#L1326

_Note: to clarify if anyone is confused at looking at the above line, this JavaScript would not DO anything. CSS would just consider it a long, unknown anonymous value._

Essentially, once something is in () [] or {}, you can dump in any characters you want.

mm, is it? For me https://www.w3.org/TR/css-variables/#syntax sounds like any token (except those listed there) can be anywhere there (regardless of either kind of parens). Or does this assumed by some other paragraph? Could you, please point me, where it says about any character, or anything specific about values inside parens? (unless it's url(...) of course)?

In other words, are either of --var: ?;, --var: (?); or --var: [¾]; actually valid?

This is the relevant section up top, which I may be mis-interpreting.

The allowed syntax for custom properties is extremely permissive. The <declaration-value> production matches any sequence of one or more tokens, so long as the sequence does not contain <bad-string-token>, <bad-url-token>, unmatched <)-token>, <]-token>, or <}-token>, or top-level <semicolon-token> tokens or <delim-token> tokens with a value of "!".

So, important takeaways are:

  1. Only the top-level semicolon-token matters. I interpret that to mean "not within some other matched pair"? But I could be misinterpreting what "top-level" means.

  2. Maybe the JS example given on that page is misleading, (-foo: if(x > 5) this.width = 10; is valid.) But an unspaced = certainly is not a typical CSS token, nor is this.width. Without clarification, and given how permissive error recovery is on browsers, I don't think we can assume any parsing error for anything. We _already know_ that the @apply syntax is valid (with a set of rules), even if it has a custom application. So we know that custom properties allow for multiple semi-colons within {}; it's therefore logical that this language implies something about the permissive nature within matching (), [], {}. There's a lot we don't know here, and without knowing _what_ can go in a custom property, I think we have to assume anything can go in it, as long as it follows a particular structure ("so long as the sequence does not contain <bad-string-token>, <bad-url-token>, unmatched <)-token>, <]-token>, or <}-token>") and pass that property along to the browser. I believe the intent here is to create maximum flexibility for developers, while creating the bare minimum requirements for successful parsing.

So, you can't do this:

.bar {
  --foo: {;
  --baz: ";
}

...because the syntax wants to allow you to put in anything, including multiple semi-colons, but it wants to be able to know when you're done. That's where the matching quotes / braces / parentheses / brackets come into play. As long as you can indicate you've "closed" your syntax in some way, I believe the intention here is that you can do what you want within that syntax.

As to this:

are either of --var: ?;, --var: (?); or --var: [¾]; valid?

I don't know.

Should we treat them as valid? IMO yes. Because we don't / can't know, but we _can_ successfully parse it without too much trouble.

As for possible implementation... It's not a big problem to allow anything there until ; but then we still should handle at least strings, urls and nested {} (as they may contain ; inside). Obviously this would mean no Less code within.

A step further would be to treat it (at the parsing stage) as a sort of DR (w/o its outer {}) with just more tokens allowed (though it looks like a dead-way since you can't reuse DR-parser for something like --var: (1 > 2) / {whatever} foo;)

And finally, for something more convenient I can't honestly see anything but writing a full-featured CSS-tokeniser with some of the Less features (tokens and then their evaluation) allowed in. A biiiiiiiig problem in other words :(

Only the top-level semicolon-token matters. I interpret that to mean "not within some other matched pair"? But I could be misinterpreting what "top-level" means.

Yes I'm pretty sure it's about the terminating ;. I.e. as in: --var: ";" url(;) {;}; is valid and --var: ; {} foo; is not (the first ; by being "top-level" terminates the statement).
I'm not sure about just (;) though.

Obviously this would mean no Less code within.

My only thought is that we could maaaaybe allow interpolation syntax, in case you wanted to generate custom properties. But maybe that's sort of a "round 2 if there's interest" kind of idea.

I'm not sure about just (;) though.

I'm not either? I believe specs often have parsing details published somewhere (like token path diagrams), but I'm not sure if it exists in this case. As a stage 1 - only requiring matching {}, (), and quotes, and dumping anything into an anonymous node until a top-level ; seems fine. Matching brackets [] may not be necessary, but they're also mentioned and it's fairly trivial once all the other pieces are there.

I don't think we need to specially tokenize anything, or get into mixing this with DR stuff. After all, someone could end up with:

.weird {
  --php: ($x = 5 /* + 15 */ + 5; echo $x;);
  --example: [My DR will be --this: { 
    blah: nope;
    --never mind i gave up;
    no wait here it --is: {
      lol: cats;
    }
  }];
}

In other words, it would be better for Less to wipe its hands clean of dealing with the contents of custom properties.

Also, it's technically feasible for a Less plugin author to write a visitor plugin that takes pieces and sends matching custom properties back through the Less parser to create new nodes. All we should really do is dump the contents into a rule node and mark the rule as a custom property, then just as faithfully dump the output.

Note that if we modify the above example, THIS should be considered invalid:

.weird {
  --example: [My DR will be --this: { 
    blah: nope;
    --never mind i gave up;
    no wait here it --is: {
      lol: cats;
   // missing matching }
  }];
}

"no Less code"

Probably, but this means a downgrade - like now one can write:
--var: darken(red, 5%) + 1;
and it works, but then (for the sake of --fortran: read (*, *, iostat=ierr) radius, height;) it won't :(

We can have an option for this probably (like --oh-no-yet-another-option-for-custom-properties-to-be-parsed-one-way-or-another: on :)

Probably, but this means a downgrade - like now one can write --var: darken(red, 5%); and it works, but then (for the sake of --fortran: read (*, *, iostat=ierr) radius, height;) it won't :(

🤔 Well..... yes I see what you mean, which is why maybe interpolation of some kind might be necessary? We could basically at the parser treat things similar to:

@iostat: ierr;
--fortran: read (*, *, iostat=@{iostat}) radius, height;

// treat similar to:
--fortran: ~"read (*, *, iostat=@{iostat}) radius, height";

(With the aforementioned caveats of proper matching tokens?)

I mean.... the alternative is that we essentially recommend escaping syntax. Just would be nice for fewer modifications of in-place CSS code.

Yes, interpolation will do the trick. Though it still would be nice to a DR-ike parsing as an experimental option (I'm pretty sure many will prefer real --var: darken(red, 5%); to imaginary --javascript: 1 = 2; at least until such custom property hacking becomes widespread :)

In other words, I'm fine with both ways (separately or simultaneously).

I think I have a fairly robust implementation / solution for this. I used a number of code examples from this thread in tests. So, custom property values and unknown at-rules are essentially treated as escaped quoted values to allow interpolation. Check out #3213.

Fixed.

Was this page helpful?
0 / 5 - 0 ratings