Skip to content

feat: env(), anchor positioning, color-mix() and math functions - #61

Draft
Kristofer Baxter (kristofer-baxter) wants to merge 3 commits into
microsoft:mainfrom
kristofer-baxter:oss/functions
Draft

feat: env(), anchor positioning, color-mix() and math functions#61
Kristofer Baxter (kristofer-baxter) wants to merge 3 commits into
microsoft:mainfrom
kristofer-baxter:oss/functions

Conversation

@kristofer-baxter

@kristofer-baxter Kristofer Baxter (kristofer-baxter) commented Sep 2, 2026

Copy link
Copy Markdown

3 of 6, and a draft until #60 merges.

A pull request from a fork can only be based on main, so the diff here includes every commit beneath this one in the stack. The commit this pull request adds is ba91178, the last one on the branch. Once #60 merges the diff will reduce to that commit alone, and I will mark this ready.

Part of the split of #57.

Adds scopes for env(), where only the first argument is the environment variable name, and indices, fallbacks, comments and continuation lines are handled. Adds anchor() and anchor-size() with their keywords. Adds color-mix(), light-dark(), contrast-color() and device-cmyk() as color functions, including custom color spaces and interpolation keywords. Adds the math and timeline functions the css-values-5 draft defines.

An env() whose first argument cannot be a name, such as env(calc(x) red), env(-10px) or env(initial), is left alone rather than having a later identifier read as the name, on that line or on a following one. An environment variable name, an anchor name and a custom color space are all identifiers, so any of them may contain an escape, may begin with a digit after the two hyphens, and is not read out of the middle of a longer identifier.

anchor() takes an <anchor-side> and anchor-size() takes an <anchor-size>. Those two sets are disjoint, so they are matched separately and neither function takes the other's keywords.

color-mix() is the only color function that takes a <color-interpolation-method>, so the interpolation-keyword matcher is on it alone. The other color functions this change adds join the existing color rule, and rgb() and its neighbours tokenize exactly as before.

Every function and keyword name this change adds is asserted, so that removing one from the grammar fails a test.

Adds scopes for `@container`: the container name, size features, logical
operators, `style()` queries and `scroll-state()` queries, including
grouped and boolean forms. The body carries
`meta.at-rule.container.body.css` and both of its braces are scoped,
matching the other block at-rules.

A query in parentheses may itself be a parenthesised query, so the
groups nest rather than close at the first bracket, and
`<general-enclosed>` is parsed so a bracket inside a string is not read
as one. A `<style-query>` is parsed recursively, so `not`, grouped
queries and ranges are not read as a declaration list. A feature value
is a component value, so a function may stand in for a keyword.

`style()` holds a `<declaration-value>`, where a `;` nested in brackets
or a balanced block is part of the value. Its declaration-value region
therefore ends only at `}` or `)`, so `style(--x: [a;b])` keeps the
whole value.

The container name is a `<custom-ident>`, so it may begin with an
escape. Only `none`, `and`, `not` and `or` are reserved, so `style` and
`scroll-state` are names in their own right and only a bracket straight
after either one makes it a query function. The query is optional, so a
name may run into the body, and the prelude is a comma-separated list,
so a name may start at a comma. The
cost of a prelude is linear in its length: doubling a 12.5 KB prelude
twice roughly doubles the time each step, and 50 KB takes under half a
second, which is far past any real stylesheet.

An unknown query function is `<general-enclosed>` wherever a query is
valid, not only inside parentheses. A `)` separates the identifier that
follows it, so `)and(` needs no whitespace. A style range may be written
without spaces, value first, or chained, and its custom property name
stops at the comparison.

A size container feature takes a length or a ratio, and only
`orientation` takes a keyword, so the values `@media` accepts are not
query values here: `@container (width: fullscreen)` reads `fullscreen`
as no more than an identifier.

A malformed prelude leaks its header scope to the rest of the file,
which is what `@media` already does. `@container` has no semicolon
form, so `@container nonsense;` leaks with the rest of them.
Adds scopes for `@property` descriptors and its custom property name,
`@scope` preludes as selectors with the scope root and the scoping limit
distinguished, and the `@starting-style` body. Block at-rules whose
prelude is omitted entirely open their body correctly.

A `@scope` prelude holds a selector list, so its parentheses balance.
`@scope (:host(.a)) to (.b)` scopes as a selector rather than closing
the prelude at the inner bracket.

The scoping limit keyword is an identifier, so `t\6f` and `\t\o` are
both recognised as `to`. A hexadecimal escape is at most six digits, so
`\00000074o` is not. A custom property name ending in a hexadecimal
escape consumes the whitespace that terminates it, so the name is not
cut short.

`@scope` takes a declaration written straight into its body, without a
selector around it, and so does a `@starting-style` nested inside a
style rule, so a declaration in either position scopes as one. A
top-level `@starting-style` holds style rules, and the test uses the
nested form for that reason.

An unclosed bracket in a `@scope` prelude, as in `@scope (:host(.a){`,
carries the prelude scope further than `main` does. It leaks rather than
recovering, which is the direction this grammar wants, but it is a
difference and worth knowing about.
Adds scopes for `env()`, where only the first argument is the
environment variable name and indices, fallbacks, comments and
continuation lines are handled; `anchor()` and `anchor-size()` with
their keywords; `color-mix()`, `light-dark()`, `contrast-color()` and
`device-cmyk()` as color functions, including custom color spaces and
interpolation keywords; and the math and timeline functions the
css-values-5 draft defines.

An `env()` whose first argument cannot be a name, such as
`env(calc(x) red)`, `env(-10px)` or `env(initial)`, is left alone rather
than having a later identifier read as the name, on that line or on a
following one. An environment variable name, an anchor name and a custom
color space are all identifiers, so any of them may contain an escape,
may begin with a digit after the two hyphens, and is not read out of the
middle of a longer identifier.

`anchor()` takes an `<anchor-side>` and `anchor-size()` takes an
`<anchor-size>`. Those two sets are disjoint, so they are matched
separately and neither function takes the other's keywords.

`color-mix()` is the only color function that takes a
`<color-interpolation-method>`, so the interpolation-keyword matcher is
on it alone. The other color functions this change adds join the
existing color rule, and `rgb()` and its neighbours tokenize exactly as
before.

Every function and keyword name this change adds is asserted, so that
removing one from the grammar fails a test.
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.

2 participants