-
Notifications
You must be signed in to change notification settings - Fork 6
Preliminary JavaScript based CSS parser research
-
Potential to be used in conjunction with Polymer or Polymer Designer. This one is fairly straight forward, but it should be kept in mind that any incompatibility here is a problem.
-
Parse CSS loosely. When parsing CSS, Polymer does not require a complete or tediously comprehensive AST - there are specific nodes that Polymer cares about transforming, and the contents of those nodes do not need to be parsed to any rigorous level of detail.
-
Parse CSS quickly. All CSS parsing will eat into Polymer or Designer's startup cost.
-
Graceful error recovery. If this parser is to be used in the context of an IDE, or as part of a runtime transformation for a framework, it should probably prefer recovery over rigid error reporting.
-
Browser compatibility. Ultimately, we need to support (until further notice) IE10 and Safari 8.
If any of these goals seem incorrect, or if any goals are missing, please suggest amendments to this list!
I have tested the following libraries for compatibility against a smoke test
that is included in the PR for shady-css-parser.
Here is a list of parser options, along with uncompressed and gzip'd file sizes:
| Parser | Uncompressed | Gzip'd |
|---|---|---|
| shady-css-parser | 16K | 3.4K |
| ReworkCSS | 11K | 2.7K |
| CSSNext | ??? | ??? |
| PostCSS | 281K | 57K |
| mensch | 31K | 7.6K |
For the purposes of science, the candidates will be tested against a smoke test that can be found here.
ReworkCSS seems like an obviously good solution. The description reads:
CSS manipulations built on css, allowing you to automate vendor prefixing, create your own properties, inline images, anything you can imagine!
Sounds promising. Let's see how it works. The test looks like this:
var rework = require('./rework-css');
var fs = require('fs');
var css = fs.readFileSync('./test.css').toString();
var parsed = rework(css);
console.log(JSON.stringify(parsed, null, 2));$ node ./test.js
/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:72
throw err;
^
Error: undefined:28:3: missing '}'
at error (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:62:15)
at declarations (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:259:26)
at rule (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:560:21)
at rules (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:117:70)
at stylesheet (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:81:21)
at module.exports (/Users/cdata/repositories/google/funsies/parser-test/rework-css.js:564:20)
at Object.<anonymous> (/Users/cdata/repositories/google/funsies/parser-test/test.js:5:14)
at Module._compile (module.js:434:26)
at Object.Module._extensions..js (module.js:452:10)
at Module.load (module.js:355:32)ReworkCSS, without any help, chokes when it hits @apply(--some-mixin); on line
28 of test.css.
Fair enough. In the world of the CSS grammar, @apply is quite an anomaly. The
CSS spec does not afford for anything close to an @ rule within a ruleset:
ident -?{nmstart}{nmchar}*
property
: IDENT S*
;
declaration
: property ':' S* expr prio?
;
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration? [ ';' S* declaration? ]* '}' S*
;
ReworkCSS dutifully parses through all of the declarations it recognizes, and
when it gets to @apply it thinks that there are no more declarations, so it
tries to match the closing brace. When it finds unexpected content, it throws.
ReworkCSS is intended to be an extensible baseline parser, so I briefly considered
extending the parser. ReworkCSS's extensibility comes via a use method that is
implemented by another module. use looks like this:
Rework.prototype.use = function(fn){
fn(this.obj.stylesheet, this);
return this;
};Unfortunately, since use operates on an already-parsed AST, it won't help us
help ReworkCSS understand @apply. I tested ReworkCSS against custom properties and custom property mixins as well. The former parsed, the later did not.
It is hypothetically possible for us to submit a patch to make ReworkCSS understand ShadyCSS concepts. Such a patch would require that we broaden the definition of a declaration (possibly in violation of the CSS spec). It would probably also be necessary for us to patch ReworkCSS to facilitate a more graceful recovery from errors.
CSSNext seems like another good option. From the description:
cssnext is a CSS transpiler that allows you to use the latest CSS syntax today. It transforms new CSS specs into more compatible CSS so you don't need to wait for browser support.
I'm already a little worried, since I know that some of the syntax we are using is either not spec'd or non-standard. However, we continue in the interest of science.
Unfortunately, I was unable to build a browser-side library for CSSNext. It's
probably possible with enough tinkering. However, in my efforts, I had to manicure
the package.json due to many breaking devDependencies. When I finally got the
(fairly esoteric) webpack build working, it consistently failed to build the
"production" version of the library. The main repository claims tests are passing
in Windows, and it's certainly possible that I am experiencing some kind of
local environment problem. Until I get this resolved, I cannot report relevant
file sizes for this library.
Thankfully, the npm package without devDependencies seemed to install just
fine. The test looks like this:
var cssnext = require('cssnext');
var fs = require('fs');
var css = fs.readFileSync('./test.css').toString();
var parsed = cssnext(css);
console.log(JSON.stringify(parsed, null, 2));$ node ./test.js
/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/lazy-result.js:167
if (this.error) throw this.error;
^
CssSyntaxError: <css input>:53:16: Missed semicolon
at Input.error (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/input.js:65:21)
at Parser.checkMissedSemicolon (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/parser.js:440:30)
at Parser.decl (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/parser.js:277:18)
at Parser.word (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/parser.js:136:30)
at Parser.loop (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/parser.js:61:26)
at Object.parse [as default] (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/parse.js:21:12)
at new LazyResult (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/lazy-result.js:54:42)
at Processor.process (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/node_modules/postcss/lib/processor.js:30:16)
at cssnext (/Users/cdata/repositories/google/funsies/parser-test/node_modules/cssnext/dist/index.js:152:26)
at Object.<anonymous> (/Users/cdata/repositories/google/funsies/parser-test/test.js:5:14)
Interestingly, CSSNext didn't choke on our "ShadyCSS" syntax. Instead, it got
scared when it encountered a normal property declaration with a missing semicolon
at the end. The culprit is the font-family declaration on this line:
/*
* #pathological.comment:foo ***/
div.interesting > .combinator:not(:nth-of-type(0)) {
font-family: 'Missing Semicolon'
position: relative;
}Sadly, I was unable to find a configuration to tell CSSNext to not throw under this condition.
During the process of trimming down the test to discover if there was some subset ShadyCSS that CSSNext recognized, I discovered that CSSNext actually outputs full transformations (and not an AST). It looks like an extension to the dependency library PostCSS may be a more appropriate approach.
PostCSS is the kernel that makes CSSNext tick. The description is about as promising as any have been so far:
PostCSS is a tool for transforming styles with JS plugins. These plugins can support variables and mixins, transpile future CSS syntax, inline images, and more.
Happily, PostCSS has a very well kept dependency tree and build script. I was
able to npm install and gulp. There are tests, and they all look green. So far
so good.
Slightly less happily, PostCSS does not appear to be intended for browser use. Not only is it a large base library, but adding dependencies (without any plugins) when browserifying makes the library notably larger than others I have considered.
I want to put it to the test. Here is the test I ended up with:
var postcss = require('postcss');
var fs = require('fs');
var css = fs.readFileSync('./test.css').toString();
postcss().process(css).then(function(result) {
console.log(JSON.stringify(result, null, 2));
}).catch(function(error) {
console.error(error);
});$ node ./test.js
{ [CssSyntaxError: <css input>:53:16: Missed semicolon]
name: 'CssSyntaxError',
reason: 'Missed semicolon',
# Really long source echoing omittedBy default, PostCSS suffers from the same problem that CSSNext suffered from. This makes a lot of sense, since CSSNext is really just a package of plugins for PostCSS.
In exploring PostCSS's plugin architecture, I believe it is feasible to adapt the built-in parser with an inherited implementation that would allow the test to pass eventually.
An extending plug-in is able to patch the core Parser class and substitute
discrete grammar parsing implementation. Here is an example of such a modified
Parser: https://github.com/postcss/postcss-safe-parser/blob/master/lib/safe-parser.es6.
This design makes PostCSS a feasible candidate. As long as all grammatical hooks are easy to override, the sky is virtually the limit since any arbitrary parser could be implemented within PostCSS itself.
Mensch is a CSS parser that we found early on while drudging up existing CSS parsers to potentially use in Designer. From the README, Mensch is:
A decent CSS parser.
I can appreciate the humility after the hyperbole of some other readme files I've read today. Let's see how it does. Here is the test:
var mensch = require('mensch');
var fs = require('fs');
var css = fs.readFileSync('./test.css').toString();
var parsed = mensch.parse(css);
console.log(JSON.stringify(parsed, null, 2));Mensch yielded a very nice AST while gracefully recovering from error conditions.
While Mensch successfully parsed the entire stylesheet, it incorrectly parsed some of the "ShadyCSS" syntax.
The custom property was parsed correctly:
{
"type": "property",
"name": "--some-var",
"value": "red"
}And at first glance, so was the custom property mixin:
{
"type": "property",
"name": "--some-mixin",
"value": "color: red"
}However, upon encountering the closing brace of the mixin, Mensch must have assumed that it had found the end of the current ruleset. The semicolon for the first mixin bleeds into the property name of the next mixin, and the mixin is also parsed as a new selector:
{
"type": "rule",
"selectors": [
";--another-mixin:"
],
"declarations": [
{
"type": "property",
"name": "color",
"value": "blue"
}
]
}Happily, Mensch appears to be okay with properties that have only a value and
no name, so it safely parsed @apply as a declaration:
{
"type": "rule",
"selectors": [
"p.mixed-in"
],
"declarations": [
{
"type": "property",
"value": "@apply(--some-mixin)"
}
]
}Mensch does not support any kind of extensibility via plugins.
Mensch's implementation is fairly small and does not depend on external libraries. Any design questions aside, it would be feasible to patch Mensch to support ShadyCSS constructs.