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

First draft #17

Merged
merged 10 commits into from
Jun 21, 2017
Merged

First draft #17

merged 10 commits into from
Jun 21, 2017

Conversation

kof
Copy link
Member

@kof kof commented May 30, 2017

This is mainly to discuss the details of it and raise all concerns. cc @geelen

@kof kof mentioned this pull request May 30, 2017
@kof kof force-pushed the first-draft branch 4 times, most recently from 53e5f69 to d44662a Compare May 30, 2017 13:17
readme.md Outdated
```js
[
[RULE_START],
[RULE_TYPE, 1],

Choose a reason for hiding this comment

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

Merge this value into the previous token ([RULE_START 1]); no need to wait until the second token of the rule to find out what type of rule it is. Then you can drop RULE_TYPE entirely.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, thought about that too.

readme.md Outdated
[PROPERTY, 'color'],
[VALUE, 'red'],
[RULE_START],
[SELECTOR, REF, ':hover'],

Choose a reason for hiding this comment

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

This structure doesn't seem to let you distinguish between & :hover and &:hover. I suggest the SELECTOR rule should actually break down the selector structurally.

Maybe after the SELECTOR, an alternating sequence of compound selectors and combinators, where each compound selector is an array of simple selectors, which are strings?

That is, &:hover would produce [SELECTOR ["&", ":hover"]], while & :hover would produce [SELECTOR ["&"] " " [":hover"]].

Copy link
Member Author

@kof kof May 30, 2017

Choose a reason for hiding this comment

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

true that, maybe a spacer marker?

'& :hover' -> [SELECTOR, REF, SPACE, ':hover']
'&:hover' -> [SELECTOR, REF, ':hover']

Choose a reason for hiding this comment

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

How would the selector &.foo.bar .baz be encoded?

(I'm trying to push you to a consistent, extensible selector representation that minimizes the amount of hand-parsing end-users have to do, which maximizes the likelihood that they'll parse things correctly.)

Copy link
Member Author

Choose a reason for hiding this comment

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

&.foo.bar .baz -> [SELECTOR, REF, '.foo', '.bar', SPACE, '.baz']

bad idea?

I'm trying to push you to a consistent, extensible selector representation

Totally appreciate that!

readme.md Outdated
[
[RULE_START],
[RULE_TYPE, 1],
[SELECTOR, SCOPED, '.foo'],

Choose a reason for hiding this comment

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

Similarly, is there some restriction of one-class-per-selector in effect here? Otherwise, you can't tell what's supposed to be scoped; or are all the classes meant to be? Less sure what the right answer would look like here, tho.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking that we can only have one class here, there is an example with multiple classes in one selector.

Copy link
Member Author

Choose a reason for hiding this comment

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

so if we need multiple classes in one selector it would looke like this:

[SELECTOR, SCOPED, '.foo']
[SELECTOR, SCOPED, '.bar']

Choose a reason for hiding this comment

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

That indicates a selector like .foo-XXX, .bar-XXX, right? I was talking about a selector like .foo-XXX.bar-XXX, where they're both in the same compound selector.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah good point!

Copy link
Member Author

Choose a reason for hiding this comment

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

What about this?

All scoped: .foo.bar -> [SELECTOR, SCOPED, '.foo', '.bar']
Some scoped: .foo.bar -> [SELECTOR, [SCOPED, '.foo'], '.bar']

readme.md Outdated
[SELECTOR, '.foo'],
[PROPERTY, 'border'],
[VALUE, '1px solid red'],
[VALUE, '1px solid green'],

Choose a reason for hiding this comment

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

I like the recognition of list-valued properties here, and the representation as distinct values. A+.

Copy link
Member Author

Choose a reason for hiding this comment

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

What I personally dislike is the use of shortcuts. I think we need to come up with a format completely without shortcuts.

readme.md Outdated
[
[RULE_START],
[RULE_TYPE, 1],
[SELECTOR, SCOPED, '.foo'],
Copy link
Member Author

@kof kof May 30, 2017

Choose a reason for hiding this comment

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

Another thought in regard of SCOPED. Actually if we want to let the consumers generate selectors, this thing is just a NAME.

We need a concept of a named rule: foo {color: red}, where foo is any user defined name.

Not scoped selector: .foo -> [SELECTOR, '.foo']
Named: foo -> [NAME, 'foo']

A rule may have just one name, but may have multiple selectors and a name.

Choose a reason for hiding this comment

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

Ah, that makes sense. So a "name" is gonna be auto-generated with some prefix, to help ensure there are no accidental name clashes, right? That's why "scoped" rules only need a single class - that's basically by definition. In that case, yeah, a separate command that is morally equivalent to a selector, but should only appear at most once per rule, sounds best.

Copy link
Member Author

@kof kof May 30, 2017

Choose a reason for hiding this comment

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

"name" is gonna be auto-generated with some prefix

This is one possibility, though I was thinking to let consumers generate them. Generating prefixes at build time on the publisher side would mean we need to ensure the uniqueness for the entire world. Leaving it up to consumer in a specific project will reduce the probability for clashes to the project context. Also there are multiple ways to generate an id, counter based is the most efficient, hashes - not so much.

@kof
Copy link
Member Author

kof commented May 31, 2017

Updated the spec.

- No separate RULE_TYPE
- NAME
- REF=>PARENT_SELECTOR
- SPACE
readme.md Outdated
const PROPERTY = 4
const VALUE = 5
const PARENT_SELECTOR = 6
const SPACE = 7

Choose a reason for hiding this comment

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

You're gonna want more than SPACE if you're going this way - you want all five of the combinators, at least. Are you expecting this to be extensible by libraries, or always limited to the set of official CSS ones?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, maybe we need a COMBINATOR which can have these values: ' ', '>', '>>', '+', '~']

Better ideas?

Copy link
Member Author

Choose a reason for hiding this comment

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

Or otherwise separate constant for each combinator.

Copy link
Member Author

Choose a reason for hiding this comment

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

As the combinators amount is limited to 5, I would probably go for separate constants, because this will make the result more compact.

readme.md Outdated

Selector compound e.g. `.foo.bar` => `[SELECTOR, '.foo', '.bar']`

Multiple classes e.g. `.foo .bar` => `[SELECTOR, '.foo'], [SELECTOR, '.bar']`

Choose a reason for hiding this comment

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

This isn't right; the description down below is that you generate multiple [SELECTOR ...] tokens to represent comma-separated selectors, like .foo, .bar, which evaluate independently.

(The terms you want, btw, are "compound selector" for things like .foo.bar, "complex selector" when combinators are involved like .foo .bar, and "X selector list" when commas are involved, like .foo, .bar.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, so this example should be

Selector list e.g. .foo, .bar => [SELECTOR, '.foo'], [SELECTOR, '.bar']

readme.md Outdated

Denotes a selector or selector compound.

Selector compound e.g. `.foo.bar` => `[SELECTOR, '.foo', '.bar']`

Choose a reason for hiding this comment

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

How do plan to handle pseudo-classes with arguments, particularly ones that take selectors as arguments like :matches(.foo, .bar)?

Copy link
Member Author

@kof kof Jun 3, 2017

Choose a reason for hiding this comment

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

Idea: FUNCTION constant followed by a function name and arguments

[FUNCTION, ':matches', '.foo', '.bar']

When combined with another selector:

.bla:matches(:hover, :active) => [SELECTOR, '.bla', [FUNCTION, ':matches', ':hover', ':active']]

@kof
Copy link
Member Author

kof commented Jun 3, 2017

Updated the spec

@kof
Copy link
Member Author

kof commented Jun 6, 2017

@tabatkins btw. this format could be also fixing the interoperability between less, sass and cssinjs!

@kof
Copy link
Member Author

kof commented Jun 8, 2017

@tabatkins @geelen do you have more thoughts about this PR?

@geelen
Copy link

geelen commented Jun 14, 2017

This is starting to shape up! I noticed one thing missing—dynamic values. For styled components, we need the ability for parts of the CSS to be injected based on props or theme variables, and if the intermediate format doesn't support that then we'd not be able to publish cross-compatible components—we'd have to publish full Styled Components.

So, I think it's worth considering areas where dynamic injections might occur, and a way to invert control so that injections can be evaluated as a snippet is executed. It could be as simple as something like:

.foo-123456 {
  color: ${ props => props.red ? 'red' : 'blue' };
  margin: 0.25em;

  ${ props => props.big && css`
    font-size: 1.25em;
    margin: 0.5em;
  }
}
[
  [RULE_START, 1],
    [RULE_NAME, 'foo'],
    [PROPERTY, 'color'],
    [VALUE, [ DYNAMIC, 0 ]],
    [DYNAMIC, 1],
  [RULE_END]
]

Then the specification also defines evaluation contexts something like:

function toCSS( rulesToSelectorsFn, ...dynamicFns ) {}

By default rulesToSelectorsFn could be rule => `._${rule}_${hash(rule)}` but that could be overridden... somehow. Dynamic interpolations would then just be referenced by their index into the dynamicFns array. When something like our babel transform generates this intermediate format, we wire up this array to make sure things are in the right order.

@geelen
Copy link

geelen commented Jun 14, 2017

Might be good to get @philpl's thoughts here now too

@kof
Copy link
Member Author

kof commented Jun 14, 2017

Definitely, I wanted just to cover basics first.

@geelen
Copy link

geelen commented Jun 14, 2017

Then my only feedback is whether any rules should have any nested values (i.e. FUNCTION) at all. Would there be benefits to keeping everything flat? Could we collapse [SELECTOR, '.bla', [FUNCTION, ':matches', ':hover', ':active']] into simply [SELECTOR, '.bla:matches(:hover, :active)']? Alternatively:

[
  [RULE_START, 1],
    [COMPOUND_SELECTOR_START],
      [SELECTOR, '.foo'],
      [FUNCTION_SELECTOR, ':matches'],
        [SELECTOR, ':hover'],
        [SELECTOR, ':focus'],
      [FUNCTION_SELECTOR_END],
    [COMPOUND_SELECTOR_END],
    [PROPERTY, 'color'],
    [VALUE, 'red']
  [RULE_END]  
]

That way any SELECTOR could be a RULE and namespacing would apply.

readme.md Outdated
[PROPERTY, 'color'],
[VALUE, 'red'],
[RULE_START, 1],
[PARENT_SELECTOR, '.bar', '.baz', SPACE_COMBINATOR, '.bla'],
Copy link

Choose a reason for hiding this comment

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

This could also be replaced with the idea of a COMPOUND_SELECTOR

@kitten
Copy link
Member

kitten commented Jun 15, 2017

By css flattener, I mean sth that potentially takes this css last and replaces nested rules with flattened ones. If nested rules will actually be parsed by a reference implementation? If not, it'll probably come down to a separate implementation that outputs this format/ast.

As to the second point: this is exactly what I'll start working on. One library which can preprocess and extract css bits in libraries like SC, glamorous and Glam.

@kof
Copy link
Member Author

kof commented Jun 15, 2017

By css flattener, I mean sth that potentially takes this css last and replaces nested rules with flattened ones.

We got nesting into this spec already, meaning RULE_START can happen inside of another rule and to reference the parent selector we got PARENT_SELECTOR. Or maybe I don't quite understand what you mean and need an example.

As to the second point: this is exactly what I'll start working on. One library which can preprocessing and extract css bits in libraries like SC, glamorous and Glam.

Sounds great!

@kitten
Copy link
Member

kitten commented Jun 15, 2017

@kof that sounds like what I mean :) maybe I'll start with writing a minimal parser and we can think about a reference implementation later ;)

added COMPOUND_SELECTOR_START
added COMPOUND_SELECTOR_END
added COMPOUND_VALUE_START
added COMPOUND_VALUE_END
added FUNCTION_START
added FUNCTION_END
@kof
Copy link
Member Author

kof commented Jun 15, 2017

Spec updated: removed nesting, array tuples length are now max 2. So now we could actually apply further optimizations, please vote with thumbs up and down to the next comments.

@kof
Copy link
Member Author

kof commented Jun 15, 2017

Should we go for single array? As consequence we will only be able to express this:

  1. [MARKER]
  2. [MARKER, value]

Right now it seems to work nicely.

[
  [RULE_START, 1],
    [SELECTOR, 'body'],
    [PROPERTY, 'color'],
    [VALUE, 'red'],
  [RULE_END]
]

will become

[
  RULE_START, 1,
    SELECTOR, 'body',
    PROPERTY, 'color',
    VALUE, 'red',
  RULE_END
]

@kof
Copy link
Member Author

kof commented Jun 15, 2017

Should we go for all string values? Benchmark shows a considerable perf benefit in v8, if it is not flowed: https://esbench.com/bench/592d599e99634800a03483d8

[
  1, 1,
    2, 'body',
    3, 'color',
    4, 'red',
  5
]

will become

[
  '1', '1',
    '2', 'body',
    '3', 'color',
    '4', 'red',
  '5'
]
  1. I am not sure if its worth it from the performance perspective (because only v8 and on micro level)
  2. Tested storage savings, using 100kb probe it shrinks down to 80kb => 20%, not sure if its worth. Probe was [1,1,2,"body",3,"color",4,"red",5] => 1\n1\n\n2\n\nbody\n\n3\n\ncolor\n4\nred\n5
  3. We will need a separate array for fn refs or any other refs

@kof
Copy link
Member Author

kof commented Jun 15, 2017

Btw. we potentially have not only fn refs, but any other refs, objects, imported primitives, whatever. If static analyzer is not able to completely follow the import which might be also a result of a complex function invocation, we will need to inline the ref in place or push all refs into a separate array. Right now the only reason to maintain separate array is the perf benefit in v8 and 20% storage savings.

@kitten
Copy link
Member

kitten commented Jun 15, 2017

@kof the perf will likely not be worse in any other engine due to a homogeneous array. So I'd strongly advise it.

For the refs we can just create a bucket. Probably a WeakMap, where it's available? Since we know the type from the AST, we don't need to separate the map for separate types

@kof
Copy link
Member Author

kof commented Jun 15, 2017

will likely not be worse

not worse, just no benefit

Probably a WeakMap, where it's available

I don't see how a WeakMap can be useful here.

@kitten
Copy link
Member

kitten commented Jun 15, 2017

@kof Yea, actually a normal Map would suffice, and a WeakMap might actually not be working correctly for our needs ^^" It was more of an example. The point is, we can create a bucket and hold all of our referenced values in there

@kof
Copy link
Member Author

kof commented Jun 15, 2017

The point is, we can create a bucket

Don't know whats a bucket in js. I guess a refs array would do + position as a pointer in the main array.

But still not sure if its worth it vs. just referencing in place.

@geelen
Copy link

geelen commented Jun 16, 2017

Yeah I wouldn't chase down perf ideas until we're sure this format can express everything it needs to. I think the idea that everything can be 2-tuples is useful because it's a design constraint that potentially speeds things up, but I'd just use nested, non-homogenous arrays to represent it. That way, if we find something that can't be done easily, we can go to 3-tuples or real references etc etc.

The performance criteria we should be judging this on is what's fastest to download + parse + convert into real CSS. 2-tuples might save us some branching in a tight loop, but it might require more structure as we're processing it. Potentially, there may be no need to deconstruct things into to [[PROPERTY, 'color'], [VALUE, 'red']], we could just store dumb strings like "color: red;". Maybe not, but it'll be much easier to understand the tradeoffs once we have one working implementation.

@kof
Copy link
Member Author

kof commented Jun 16, 2017

we could just store dumb strings like "color: red;".

I think we need to deconstruct things heavily, otherwise you want be able to for e.g. vendor-prefix props/values upon consumption efficiently.

I was even thinking about deconstructing numeric values and its units.

@kof
Copy link
Member Author

kof commented Jun 16, 2017

Btw. even in a flatten array we still can have longer than 2 value pairs, we basically can't have dynamic amount of members, but rather a statically predefined for a certain marker.

@kof
Copy link
Member Author

kof commented Jun 16, 2017

Ok, lets flatten the format and try other optimizations like homogeneous array after we have one reference implementation and see what else is missing, I will create for them separate issues.

@kof
Copy link
Member Author

kof commented Jun 16, 2017

I think we can merge this PR and create new one for the next things. Thumbs up?

@kof kof merged commit 9be52bc into master Jun 21, 2017
@kof
Copy link
Member Author

kof commented Jun 21, 2017

merged 🎉

@kof kof deleted the first-draft branch July 1, 2017 11:44
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.

4 participants