Repository navigation
CSS Syntax Foundation
OPBrowser owns CSS tokenization, selector/declaration parsing, author matching, cascade and
the current computed-style subset in op_css. Supported values now feed layout and paint;
this page records the syntax/matching boundary rather than a parser-only prototype.
CSS text
-> tokenize()
-> Token { kind, start, end }
-> parse_stylesheet() / parse_declaration_list()
-> Stylesheet
-> StyleRule
-> Selector + Specificity
-> Declaration
The tokenizer recognizes whitespace, comments, identifiers, hashes, strings and
escapes, numbers, percentages, dimensions, functions and CSS structural punctuation.
Malformed comments and strings produce recoverable CssError values with byte offsets.
Unquoted url(...) is one Url token, preserving punctuation in paths and data URLs and
decoding escapes. Quoted url() remains a function containing a String token. Internal
unescaped whitespace, quotes, parentheses, control characters and newline escapes produce
BadUrl; recovery consumes through an unescaped closing parenthesis. EOF retains the URL
value with a diagnostic. BadUrl/BadString invalidate whole declarations, including custom
properties and unused var() fallbacks; URL payloads count toward expansion storage limits.
Generated before/after content now loads URL images through the bounded image worker;
CSS background images remain outside the supported subset.
Reference: CSS URL tokenization.
The stylesheet parser keeps valid rules after malformed declarations or unsupported rules where recovery is possible. Declaration parsing preserves custom-property name case, normalizes ordinary property names to ASCII lowercase and extracts trailing !important.
The current selector AST/matcher supports:
- type, universal, class and ID selectors;
- compound and comma-separated selector lists;
- descendant and child (
>) combinators; - adjacent (
+) and general (~) element-sibling combinators; - attribute existence plus
=,~=,|=,^=,$=,*=matchers; - explicit ASCII
i/sattribute-value flags; -
:root,:first-child,:last-child,:only-child,:empty,:link; - specificity counts with attributes/pseudo-classes in the class column.
Matching runs right-to-left. Sibling combinators operate on element siblings and therefore
ignore intervening text nodes, matching CSS tree semantics. :empty still treats any text,
including whitespace text, as content. The functional/pseudo-element expansion is described
below; namespace selectors and at-rules remain explicit unsupported syntax.
The engine now collects CSS from embedded style elements and from style attributes.
The supported selectors are matched right-to-left against op_dom. Each matching element
gets StyleMap candidates rather than a prematurely resolved winner. Functional
:is()/:where()/:not() recursively reuse the same Selector matcher, while
:nth-child()/:nth-last-child() index element siblings matching optional of filters.
Selectors 4 :lang() uses case-insensitive RFC 4647-style extended filtering over the nearest
inherited HTML lang, including wildcard subtags and comma-separated ranges. :dir() currently
resolves inherited valid ltr/rtl HTML direction values; full dir=auto bidi inference is
later work. Initial :required, :optional, :open and :visited parsing/matching is present;
visited history is deliberately not exposed yet and therefore never matches.
Terminal ::before/::after are represented
as a selector target rather than fake DOM nodes and contribute type-level specificity.
DOM
-> collect <style> + style=""
-> parse author CSS
-> match supported selectors
-> MatchedDeclaration { declaration, specificity, source_order, source }
-> StyleMap host bucket keyed by NodeId
+ pseudo buckets keyed by (NodeId, Before|After)
-> retained StyleCollection in PreparedDocument
Stylesheet and inline declarations remain distinguishable so the next cascade stage can apply the correct precedence instead of encoding an approximation now. Selector-list rules are stored once per declaration using the highest specificity among selectors that matched that element. CSS parser errors are retained with the NodeId that supplied them. A style element with a non-CSS type is ignored in this initial collection layer.
Resize reflow reuses the retained StyleCollection together with the DOM and image resources, so CSS is not reparsed merely because the window changed size.
Author candidates now resolve into a ComputedStyleMap. For supported declarations the cascade key is, from strongest dimension to weakest: !important, inline-style source, specificity, then source order. Invalid values for a supported property are filtered before choosing the winner, allowing a lower-priority valid declaration to apply.
The current computed subset includes display, text color/font sizing/weight, block
backgrounds, margin/padding/border edges, width/height min/max and box-sizing. A color-only
background shorthand shares cascade winner selection with background-color. Color accepts
#RGB(A)/#RRGGBB(AA), all 148 opaque CSS named colors (including aliases/rebeccapurple), legacy comma and modern
space/slash rgb()/rgba(), hsl()/hsla() and modern hwb() with hue angle units and alpha. HWB
whiteness/blackness accept percentages or numbers on the 0..100 reference scale; nonnegative
values above 100 normalize to gray rather than clamp individually. none components resolve
to zero for current used-color painting. Comma HWB syntax is invalid. The same color
parser feeds text, background and borders. Font-size accepts percentages, px/em/rem,
absolute CSS length units and the current keyword set. Global inherit/initial/unset handling
is shared by the supported properties.
color, font-size, font-weight, line-height and text-align inherit through the element tree.
display uses its initial value unless explicitly inherited. display:contents now has an explicit
computed value; normal-flow layout suppresses the element's principal box while still traversing its
generated and DOM children with inherited style. The currently pinned simple block/generated/text,
float-wrapper, list/button/details/fieldset cases pass through this path; flex, table-internal and SVG
contents behavior still needs dedicated formatting-context work. Temporary UA defaults mirror the
existing M1 block/hidden tag rules and heading font sizes/weights, avoiding an unrelated visual
regression when layout begins consuming computed styles. The CSS hash tokenizer also accepts
digit-leading hash values required by hexadecimal colors.
CSS custom properties now have their own computed token environment. Case-sensitive --name
declarations use the same author importance/specificity/source-order cascade, inherit by
default, and resolve nested var(--name, fallback) references on the element where the custom
property is computed. The resolved map is then used to substitute var() into ordinary
property declarations before the existing value parsers run. Missing/cyclic references can use
nested fallbacks; all directed cycle participants become unavailable, including cycles in
unused fallback branches. Noncyclic consumers can still recover with fallback. initial removes
a custom property while inherit/unset reuse the parent's computed value.
Pseudo buckets cascade independently from their host. A generated pseudo starts from the
host's inherited text and custom properties, applies its own declarations, and becomes a
ComputedPseudoStyle { style, content } only when a supported content value generates text.
The current content subset concatenates quoted strings with attr(name), counter() and
counters(). attr() reads the originating element's DOM attribute and returns an empty string
when that attribute is absent. counter-reset, counter-set and counter-increment mutate a
scoped document-order CounterContext before generated content is resolved; nested reset stacks
feed counters() while sibling increments remain visible inside the same scope. Decimal,
decimal-leading-zero, lower/upper alpha/latin and lower/upper roman formats are supported.
none and normal suppress creation. Because var() substitution happens first, generated
content and counter declarations may come from inherited or pseudo-local custom properties. PreparedDocument retains both the author StyleCollection and
ComputedStyleMap, including host/pseudo CustomPropertyMap snapshots.
Resize reflow therefore does not reparse, rematch or recascade CSS.
ComputedStyleMap is now consumed by op_layout. display:none removes the subtree from
layout, display:block creates a flow boundary, display:inline stays in the current inline
flow, and inline-table/table/table-caption/table-column-group/table-column/table-header-group/
table-row-group/table-footer-group/table-row/table-cell feed the table formatting machinery.
display:inline-table is parsed separately from display:table and enters inline flow as one atomic
object rather than forcing a block break. border-spacing accepts one/two nonnegative lengths and
inherits; border-collapse accepts separate/collapse and inherits. table-layout accepts auto/fixed
and is non-inherited; caption-side accepts top/bottom and inherits. Explicit-width fixed tables use
column hints and first-row widths without late-row intrinsic resizing, while auto tables retain
percentage constraints alongside content preferences. Captions are placed above or below the table
border box according to caption-side. vertical-align currently recognizes baseline/top/middle/
bottom as a non-inherited property; table cells use those values internally and atomic inline-table
boxes now carry the same values into line placement. Table UA style starts separate at 2px/2px.
These table properties reach the grid formatter, including zero spacing, shared-cell border
resolution and post-row vertical content placement.
The formatter performs anonymous row/cell child fixup inside display:table and normal-flow
collection groups consecutive orphan table-internal siblings under one anonymous block table,
including initial caption and column-hint repair, so CSS-generated table structures can recover
missing wrappers without altering the DOM. Structural display:contents wrappers are expanded for
this fixup only when their exposed non-whitespace descendants are table-internal; ordinary contents
containers remain in the inline/block collection path and continue to carry inherited styles. font-size, font-weight/style, line-height,
decoration, spacing, transform and
color are carried on inline character runs. text-align offsets completed lines, while
white-space controls collapse/newline preservation/soft wrapping. text-transform is applied
before measurement and link-range reconstruction; spacing changes wrapping and native paint.
Italic styling participates in both measurement and GDI font creation.
Text color reaches op_paint and the Win32 painter through TextBox/paint commands. Block
background, margin/padding, sizing/box-sizing and independent solid border sides also flow
through computed style into layout BoxDecoration geometry and platform-neutral FillRects.
Non-replaced inline elements now resolve background-color/padding/solid borders into an
InlineBoxStyle, contribute those extras to line fitting, and emit per-line BoxDecoration
fragments before text painting. Generated ::before text is inserted before real children and
::after after them; both use the same line formatter, text transforms/spacing, and fragment
paint path. CSS2 single-colon spellings :before, :after and :first-letter map to the same
terminal pseudo-elements as their double-colon forms. ::first-letter is parsed and cascaded
separately from generated content, then layout
applies its explicitly-authored inline properties to the first non-whitespace Unicode grapheme
cluster. This keeps Regional Indicator pairs atomic and avoids overwriting color/font inheritance
coming through display:contents when the pseudo did not author those properties. Inline box
identity includes the generated pseudo target so generated and host decorations stay distinct.
Alpha text/box colors are currently composited over the white page background. The existing
hyperlink glyph/underline path uses computed color and decoration. Anchors with href have
blue/underlined UA defaults before author cascade; nested/generated text can override them.
visibility:visible/hidden now participates in the inherited computed-style path. Hidden content
retains its layout geometry; text/images carry a non-painting visibility bit to the display-list
builder, and box colors are suppressed without changing box dimensions. Anchors without href retain
ordinary inherited presentation.
External <link rel="stylesheet"> resources now join embedded rules at their actual DOM
positions before selector matching, so stylesheet source order crosses file boundaries.
The loader supports bounded local/file/data/HTTP(S) CSS and retains the resulting author
and computed styles across resize reflow. See Stylesheet Loading
for activation rules, decoding, security policy and budgets.
The local demonstration page is examples/css/index.html; it now links a real
examples/css/theme.css and uses a later embedded override to expose source order.
The built-in start page also describes the supported subset in a normal release launch.
The expanded block-level box model is documented in CSS Block Box Model.
Initial text alignment, line-height, font-style, underline/line-through, white-space,
letter/word spacing, text-transform and inline background/padding/solid-border fragments now
reach layout/native paint. Functional :is()/:where()/:not() and :nth-child(An+B) now
participate in selector matching with their initial specificity rules. Terminal
::before/::after, generated strings/attr()/counters, inherited custom properties and
var() fallbacks now reach native layout/paint too. Counter traversal computes ::before
before children and ::after after completed child counter work. Computed var() failures retain
their cascade priority and become unset for supported properties. Language-aware automatic quotes,
custom counter styles and complete counter scoping remain later. Next S3 work moves into generated
replaced content and nested-inline geometry, then relational selectors,
advanced color spaces, at-rules, media queries and full CSS
conformance remain later.
quotes inherits and accepts auto, none or one or more opening/closing string pairs,
plus initial/inherit/unset and var() substitution. Pseudos can override their host pairs.
The content keywords open-quote/close-quote emit the appropriate pair and change nesting;
no-open-quote/no-close-quote change nesting without glyphs. quotes:none suppresses glyphs
while preserving those nesting effects. Depth is shared in emitted document order, the last
pair repeats at deeper levels and closing at depth zero does nothing. Losing/invalid content
candidates, display:none subtrees/pseudos and content:none/normal never change that state.
The same hidden/absent-box exclusion applies to counter mutations. <q> gets default
open/close pseudos, overridable by author content. auto currently chooses deterministic
English Unicode pairs; language-specific selection remains future work.
Behavior reference: CSS quotation marks.
Generated display:block now routes retained pseudo text through the ordinary block geometry
path instead of approximating a block with line breaks. It honors widths/heights/min/max,
percentages, box-sizing, auto/negative margins, padding, solid borders and backgrounds.
An empty string still creates the block box, allowing CSS-only rules and bars without glyphs.
Definite heights control normal flow and border/background extents even when text overflows.
Parent/child and empty-block margin collapse and full replaced/nested-inline geometry remain later.
op_css::custom constructs a directed dependency graph per element/pseudo, including every
var() reference in fallback branches even if that fallback would not be selected. Iterative
Kosaraju SCC traversal invalidates exactly cycle participants; dependent values can recover
using their own fallback. Computed inherited values contain no var() references, preserving
their original meaning when descendants override other names. Valid empty custom values
substitute an empty token stream instead of selecting a fallback; bare -- names are rejected.
Substitution permits at most 16,384 tokens and 256 KiB token storage (enum payload plus string bytes) per value, 2 MiB retained resolved values per element/pseudo and 64 nested fallback levels. Expansion checks happen before cloning tokens. Oversized values become invalid and ordinary consumers can select fallback values. Graph traversal and dependency resolution do not recurse on the native stack; tests exercise a 10,001-variable chain and exponential expansion. Broader custom-property grammar, registration and animation-taint handling remain later. Reference: CSS variable cycles and length limits.
Computed declaration copies carry value_from_var so failed/empty substitutions and values that fail the supported property's grammar resolve as unset while retaining importance, specificity and source order. Shorthands compete separately for each longhand, preserving later overrides. Literal parse-time invalid values still allow lower valid declarations. Invalid content winners suppress pseudos and their counter mutations; invalid quotes inherit and invalid counter operations reset to their initial empty list. var() syntax is validated before cascade, including references inside unused fallback branches. A table-driven regression compares missing/wrong-type/empty var() results with explicit unset across all 52 supported ordinary style property names; Engine tests verify inherited/initial paint and retained reflow.
:is()/:where() discard invalid/unsupported/pseudo-element branches individually. Empty
or all-invalid argument lists are valid and match nothing; discarded branches contribute no
specificity. :not(), nth of filters and ordinary top-level lists remain strict. :has() now
accepts strict relative selectors beginning with an implicit descendant relation or explicit
>/+/~, walks forward from the anchor, and uses the maximum argument specificity. Nested
:has() and pseudo-elements inside :has() are rejected. Nested functional selectors are bounded
to 64 levels before recursive parsing/matching.
:nth-child(An+B of selector-list) and :nth-last-child(...) filter inclusive element
siblings using the ordinary complex-selector matcher, count each element once even if several
branches match, and index from the front/back. Their specificity adds one pseudo-class plus
the maximum filter specificity, independent of the matching branch. An+B parsing uses integer,
n-dimension and n-ident token grammar, preserving sign/whitespace rules rather than joining
arbitrary tokens. Coefficients are bounded to i32 and arithmetic uses i64.
Reference: Selectors 4 and
CSS An+B syntax. Empty-namespace attribute
syntax [|name] is accepted for ordinary HTML attributes while whitespace between | and the name
remains invalid.
Empty generated inline strings and empty DOM inline elements with padding or borders produce real decoration geometry through an EmptyInline formatter item. They reserve horizontal edges, participate in wrapping/nowrap/alignment and expand safe line extents using font/vertical-edge metrics while emitting no fabricated text or link spans. A background by itself on zero content has zero fragment geometry and therefore neither creates a line nor interrupts collapsible spaces.
Typed first/last/only-of-type pseudo-classes filter inclusive element siblings by the
candidate's HTML tag name. nth-of-type/nth-last-of-type reuse the same token-aware An+B
parser and indexing arithmetic, with same_type metadata instead of an authored of-list.
Other element types and text do not affect positions. All five contribute one class-level
specificity component; typed nth functions do not accept of arguments. Namespace-aware
typed selector behavior remains outside the current HTML-only selector subset.
HWB uses shared unquantized HSL channels before final 8-bit sRGB conversion. Hue angles normalize before unit scaling, so large finite turns cannot overflow the color conversion. Missing-component preservation for general interpolation/serialization and calc() inside colors remain later work. Reference: CSS Color 4 HWB.
Named colors use allocation-free ASCII case-insensitive binary search in op_css::named. Packed names plus six-byte RGB records total 2,210 static bytes. The full source is pinned in crates/op_css/data/named-colors.tsv; the offline generator verifies 148 names/139 distinct RGB values, aliases and import-time hexadecimal/decimal agreement. Normal builds use the checked-in Rust data. Transparent/currentcolor remain separate special keywords. Reference: pinned CSS Color named colors.
CSS Color 4 device-independent and predefined spaces now participate in ordinary computed
color: lab(), lch(), oklab(), oklch(), color(srgb ...), color(srgb-linear ...), display-p3,
display-p3-linear, a98-rgb, prophoto-rgb, rec2020 and xyz/xyz-d50/xyz-d65. Percentage
reference ranges, D50/D65 chromatic adaptation, RGB transfer curves and matrix conversions
live in op_css::color; final used colors are clipped/rounded to the current 8-bit sRGB paint
target. Plain currentColor resolves from the same element's computed color for background/borders,
and on the color property uses the inherited foreground. Background computed values can now retain
a private currentColor-dependent expression across inherit, then resolve it against the receiving
element. The initial relative-color slice supports the pinned from currentColor RGB/HSL/Lab/OKLab
and predefined/XYZ identity-channel forms plus literal HSL hue replacement. color-mix() currently
supports two color stops in sRGB or LCH, percentage normalization, alpha scaling for totals below
100%, and deferred currentColor operands. Broader interpolation spaces, arbitrary relative channel
math/calc(), missing-component preservation and perceptual gamut mapping remain later work.
The same values feed text/background/border/pseudo cascade, var() substitution, painting and
retained reflow. CSS system colors use a deterministic browser-owned palette, with deprecated
CSS2 system names mapped to their CSS Color 4 modern equivalents; fixed system values are used
instead of exposing host theme details. Simple declaration-form @supports (property: value)
can now gate nested rules for the supported color declarations; boolean/composed supports
conditions remain later work. Reference: CSS Color predefined spaces.
Modern RGB/HSL also accept none channels/alpha through the shared modern argument parser. RGB may mix numeric and percentage channels in modern syntax; comma syntax requires all three channels to be numbers or all percentages. Modern HSL saturation/lightness accept numbers on the 0..100 percentage reference scale and clamp before conversion; comma HSL retains percentage-only saturation/lightness. Legacy forms reject none and mixed slash/ comma grammar. Invalid literals allow lower-priority valid declarations to win; invalid var() winners resolve as unset. Reference: CSS Color RGB/HSL syntax.
Проект
Архитектура
Реализация
- CSS Syntax
- CSS Box Model
- Inline Layout
- Stylesheets
- Images
- Source Loading
- HTML Encoding
- Reflow
- Navigation
- JavaScript VM
- Request Filtering
Тесты и разработка