Skip to content

Commit 36b3170

Browse files
jhaygood86claude
authored andcommitted
CssData: index nested and layered rules; resolve colours through the engine
Three consumer-side changes the refreshed engine needs to actually take effect. CssData.IndexRules now recurses into IStyleRule.NestedRules and ILayerRule.Rules. A CSS-Nesting child rule lives in its parent's NestedRules, deliberately outside Children, so it is invisible to Stylesheet.Rules - without the recursion every nested rule would parse and then silently never apply. Likewise, once `@layer name { ... }` parses into a LayerRule, its contents would fall through the if/else chain and stop applying; they are now indexed as ordinary unlayered rules. Layer *precedence* is still not implemented. CssData.FlattenStyleRules replaces the top-level-only Stylesheet.StyleRules scan in DomUtils.WriteStylesheet/CollectSimpleSelectorStyles and DomParser.SetTextSelectionStyle, so a ::selection rule nested inside @layer or another rule is still found. CssParser.ParseStyle no longer lets a bare `@layer a, b;` statement close the @import prologue - that is legal before @import per CSS Cascade 5, and treating it as a non-import rule silently skipped every @import after it. CssValueParser.GetColorByName falls back to the engine's ToResolvedColor when the adapter's palette does not recognize a value, so lab()/oklab()/lch()/ oklch()/color-mix() resolve to real colours everywhere a colour is parsed instead of coming out black. It also fixes `transparent`, which the palette reports with a zero alpha and which was previously rejected as invalid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8e4bb05 commit 36b3170

5 files changed

Lines changed: 89 additions & 11 deletions

File tree

‎Source/HtmlRenderer/Core/CssData.cs‎

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@ public IndexedRule(IStyleRule rule, bool isUserAgent, int documentOrder, MediaLi
129129
private Dictionary<string, List<IndexedRule>> _idIndex;
130130
private List<IndexedRule> _universalRules;
131131

132+
/// <summary>
133+
/// Every style rule in a rule list, in document order, descending into <c>@media</c> and
134+
/// <c>@layer</c> blocks and into CSS-Nesting child rules. <see cref="Stylesheet.StyleRules"/>
135+
/// only sees the top level, so callers that scan for a rule by selector (rather than matching
136+
/// against a box) need this or they miss anything nested. <c>@supports</c>/<c>@container</c> are
137+
/// deliberately not descended into: their conditions are not evaluated, so their contents do not
138+
/// apply.
139+
/// </summary>
140+
internal static IEnumerable<IStyleRule> FlattenStyleRules(IEnumerable<IRule> rules)
141+
{
142+
foreach (var rule in rules)
143+
{
144+
if (rule is IStyleRule styleRule)
145+
{
146+
yield return styleRule;
147+
148+
foreach (var nested in FlattenStyleRules(styleRule.NestedRules))
149+
yield return nested;
150+
}
151+
else if (rule is IMediaRule mediaRule)
152+
{
153+
foreach (var nested in FlattenStyleRules(mediaRule.Rules))
154+
yield return nested;
155+
}
156+
else if (rule is ILayerRule layerRule)
157+
{
158+
foreach (var nested in FlattenStyleRules(layerRule.Rules))
159+
yield return nested;
160+
}
161+
}
162+
}
163+
132164
private void EnsureIndex()
133165
{
134166
if (_universalRules != null) return;
@@ -152,10 +184,10 @@ private void EnsureIndex()
152184
}
153185

154186
/// <summary>
155-
/// Walks a rule list in true document order, recursing into <c>@media</c> blocks (any
156-
/// nesting depth) in place, and buckets every style rule found - assigning each an
157-
/// ever-increasing <see cref="IndexedRule.DocumentOrder"/> and recording the chain of
158-
/// enclosing media conditions it's nested under, if any.
187+
/// Walks a rule list in true document order, recursing into <c>@media</c> blocks, <c>@layer</c>
188+
/// blocks and CSS-Nesting child rules (any nesting depth) in place, and buckets every style rule
189+
/// found - assigning each an ever-increasing <see cref="IndexedRule.DocumentOrder"/> and recording
190+
/// the chain of enclosing media conditions it's nested under, if any.
159191
/// </summary>
160192
private static void IndexRules(
161193
IEnumerable<IRule> rules,
@@ -200,6 +232,16 @@ private static void IndexRules(
200232
list.Add(indexedRule);
201233
}
202234
}
235+
236+
// CSS Nesting: a nested rule lives in its parent's NestedRules, deliberately not in
237+
// Children, so it is invisible to Stylesheet.Rules and would otherwise never be
238+
// indexed. Its selector was already resolved against the parent at parse time, so it
239+
// indexes like any other rule - under the same media context, and with a document
240+
// order that continues from the parent so it tie-breaks after it.
241+
if (styleRule.NestedRules.Count > 0)
242+
{
243+
IndexRules(styleRule.NestedRules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order);
244+
}
203245
}
204246
else if (rule is IMediaRule mediaRule)
205247
{
@@ -208,6 +250,18 @@ private static void IndexRules(
208250
: enclosingMedia.Concat(new[] { mediaRule.Media }).ToArray();
209251
IndexRules(mediaRule.Rules, isUserAgent, nestedMedia, tagIndex, classIndex, idIndex, universal, keys, ref order);
210252
}
253+
else if (rule is ILayerRule layerRule)
254+
{
255+
// @layer's contents are indexed as ordinary, unlayered rules: they apply, ordered by
256+
// source position and specificity. Layer precedence, !important layer reversal and
257+
// revert-layer are deliberately not implemented - that needs a layer-banded cascade,
258+
// which is separate work. Without this branch the block would fall through and every
259+
// rule inside a layer would silently stop applying.
260+
IndexRules(layerRule.Rules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order);
261+
}
262+
// Everything else (@layer statements, @property, @font-palette-values, @supports,
263+
// @container, ...) contributes no style rules to the cascade, so falling through is the
264+
// correct handling rather than an oversight.
211265
}
212266
}
213267

‎Source/HtmlRenderer/Core/Parse/CssParser.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,10 @@ private void ParseStyle(CssData data, string stylesheetText, Uri baseUri, HashSe
243243
data.Combine(importedCssData);
244244
}
245245
}
246-
else
246+
else if (!(rule is LayerStatementRule))
247247
{
248+
// A bare `@layer a, b;` statement is allowed before @import (CSS Cascade 5 §6.4.1), so
249+
// it must not close the @import prologue - otherwise every following @import is skipped.
248250
hasReachedNonImportRules = true;
249251
}
250252
}

‎Source/HtmlRenderer/Core/Parse/CssValueParser.cs‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,13 +682,35 @@ private static bool TryParseAngleDegrees(string value, out double degrees)
682682
}
683683

684684
/// <summary>
685-
/// Get color by given name, including .NET name.
685+
/// Get color by given name (including .NET names), or by any color function the CSS engine
686+
/// understands.
686687
/// </summary>
688+
/// <remarks>
689+
/// The adapter's palette is tried first because a bare name is by far the common case and a
690+
/// dictionary lookup is cheaper than tokenizing. Anything it doesn't recognize - a CSS Color 4/5
691+
/// function such as <c>lab()</c>/<c>oklch()</c>/<c>color-mix()</c>, or a fully transparent color,
692+
/// which the palette reports with a zero alpha and is indistinguishable from "unknown" here - is
693+
/// then resolved through the engine, which is the same code path the cascade validated the value
694+
/// with. Hex/rgb()/rgba()/hsl()/hsla() never reach this method; they keep their own fast paths in
695+
/// <see cref="TryGetColor"/>.
696+
/// </remarks>
687697
/// <returns>true - valid color, false - otherwise</returns>
688698
private bool GetColorByName(string str, int idx, int length, out RColor color)
689699
{
690-
color = _adapter.GetColor(str.Substring(idx, length));
691-
return color.A > 0;
700+
var text = str.Substring(idx, length);
701+
702+
color = _adapter.GetColor(text);
703+
if (color.A > 0) return true;
704+
705+
var resolved = GetCssTokens(text).ToResolvedColor();
706+
if (resolved != null)
707+
{
708+
var value = resolved.Value;
709+
color = RColor.FromArgb(value.A, value.R, value.G, value.B);
710+
return true;
711+
}
712+
713+
return false;
692714
}
693715

694716
/// <summary>

‎Source/HtmlRenderer/Core/Parse/DomParser.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ private void SetTextSelectionStyle(HtmlContainerInt htmlContainer, CssData cssDa
778778

779779
foreach (var stylesheet in cssData.Stylesheets)
780780
{
781-
foreach (var rule in stylesheet.StyleRules)
781+
foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules))
782782
{
783783
if (!SelectorIsSelectionPseudoElement(rule.Selector)) continue;
784784

‎Source/HtmlRenderer/Core/Utils/DomUtils.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ private static void CollectSimpleSelectorStyles(CssData cssData, Func<ISelector,
901901
{
902902
foreach (var stylesheet in cssData.Stylesheets)
903903
{
904-
foreach (var rule in stylesheet.StyleRules)
904+
foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules))
905905
{
906906
if (!selectorMatches(rule.Selector)) continue;
907907
foreach (var prop in rule.Style)
@@ -940,7 +940,7 @@ private static void WriteStylesheet(StringBuilder sb, CssData cssData)
940940
sb.AppendLine("<style type=\"text/css\">");
941941
foreach (var stylesheet in cssData.Stylesheets)
942942
{
943-
foreach (var rule in stylesheet.StyleRules)
943+
foreach (var rule in CssData.FlattenStyleRules(stylesheet.Rules))
944944
{
945945
sb.Append(rule.SelectorText);
946946
sb.Append(" { ");

0 commit comments

Comments
 (0)