From af66a2fab161877349c0a2e73e121691da8b2356 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Ros?=
<1165805+sebastienros@users.noreply.github.com>
Date: Fri, 4 Sep 2026 22:34:21 -0700
Subject: [PATCH 1/2] Fix cyclic custom properties during computed style
resolution
Resolve per-element custom-property dependency components iteratively before inheritance, including unused fallback edges. Preserve computed token values, substitute complete consumer and shorthand values, and apply computed-value defaults without recursive variable evaluation.
Add regression coverage for cycles, inheritance, shared rules, CSSOM mutation, token boundaries, long chains, nested fallbacks, and bounded expansion. Document the substitution limit.
Fixes #241
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
docs/general/02-Values.md | 20 +
.../Styling/CustomPropertyCycles.cs | 415 ++++++++++++++++++
.../Dom/Internal/CssProperty.cs | 4 +-
.../Dom/Internal/CssStyleDeclaration.cs | 7 +-
.../Extensions/CssOmExtensions.cs | 33 +-
.../Extensions/DeclarationInfoExtensions.cs | 2 +-
.../Extensions/StyleCollectionExtensions.cs | 56 ++-
src/AngleSharp.Css/Parser/CssTokenizer.cs | 6 +-
.../Parser/Micro/FunctionParser.cs | 63 +--
.../RenderTree/RenderTreeBuilder.cs | 22 +-
src/AngleSharp.Css/Values/CssChildValue.cs | 39 +-
.../Values/CssComputeContext.cs | 19 +-
.../Values/CssCustomPropertyResolver.cs | 161 +++++++
src/AngleSharp.Css/Values/CssVariableValue.cs | 279 ++++++++++++
.../Values/Functions/CssVarValue.cs | 12 +-
src/AngleSharp.Css/Values/Raws/CssAnyValue.cs | 17 +-
.../Values/Raws/CssInvalidValue.cs | 22 +
.../Values/Raws/CssReferenceValue.cs | 24 +-
18 files changed, 1060 insertions(+), 141 deletions(-)
create mode 100644 src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
create mode 100644 src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
create mode 100644 src/AngleSharp.Css/Values/CssVariableValue.cs
create mode 100644 src/AngleSharp.Css/Values/Raws/CssInvalidValue.cs
diff --git a/docs/general/02-Values.md b/docs/general/02-Values.md
index c208cd27..d4fa19db 100644
--- a/docs/general/02-Values.md
+++ b/docs/general/02-Values.md
@@ -62,3 +62,23 @@ Console.WriteLine($"Computed font-size: {computedFontSize}");
- Shorthand values (e.g., `margin`, `background`) are decomposed internally to longhands.
- Variables (`var(--x)`) may defer full resolution until cascade context is available.
- Comparing raw source strings is often misleading; compare parsed or computed values instead.
+
+## Custom Properties At Computed-Value Time
+
+Custom properties are resolved for each element before they are inherited. An inherited
+alias keeps the parent's resolved value; changing its dependencies on a child does not
+resolve that alias again. A declaration explicitly matching both elements is resolved
+locally on each element.
+
+Following [CSS Variables dependency-cycle rules](https://drafts.csswg.org/css-variables-1/#cycles),
+every property in a cycle becomes guaranteed-invalid, including cycles through unused
+fallbacks. A consuming `var(--name, fallback)` can recover from an invalid or missing
+custom property. Without a usable fallback, the consuming declaration uses its inherited
+or initial value, not an earlier declaration from the cascade. A valid custom-property
+value that does not match the consumer's grammar does not trigger the `var()` fallback.
+
+Dependency analysis and fallback substitution are iterative, including deeply nested
+fallbacks. Expanded values are limited to 1,048,576 UTF-16 code units (including token
+separators) to bound exponential substitution; an expansion exceeding this limit is
+invalid at computed-value time. Property-specific parsing, unit conversion, and layout
+support still determine which resolved values can be used by a consuming property.
diff --git a/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
new file mode 100644
index 00000000..0d9a912d
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
@@ -0,0 +1,415 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Css.RenderTree;
+ using AngleSharp.Css.Values;
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Threading.Tasks;
+ using static CssConstructionFunctions;
+
+ [TestFixture]
+ public class CustomPropertyCyclesTests
+ {
+ [Test]
+ public async Task OriginalReproductionDoesNotOverflow()
+ {
+ using var context = BrowsingContext.New(Configuration.Default.WithCss());
+ using var document = await context.OpenAsync(response => response.Content(
+ "Save "));
+ var style = document.QuerySelector("button").ComputeCurrentStyle();
+ Assert.AreEqual("rgba(0, 0, 0, 1)", style.GetPropertyValue("color"));
+ Assert.AreEqual(String.Empty, style.GetPropertyValue("--a"));
+ Assert.AreEqual(String.Empty, style.GetPropertyValue("--b"));
+ }
+
+ [TestCase("--a:var(--a)")]
+ [TestCase("--a:var(--a,visible)")]
+ [TestCase("--a:var(--b);--b:var(--a)")]
+ [TestCase("--a:var(--b,visible);--b:var(--a,visible)")]
+ [TestCase("--a:var(--b);--b:var(--c);--c:var(--a)")]
+ [TestCase("--present:visible;--a:var(--present,var(--a))")]
+ [TestCase("--present:visible;--a:var(--present,calc(var(--a)))")]
+ [TestCase(@"--a:var(--\61,visible)")]
+ [TestCase(@"--a:v\61 r(--a,visible)")]
+ [TestCase("--a:var(--b,var(--c));--b:var(--a);--c:var(--b,visible)")]
+ public void EveryCyclicMemberIsInvalid(String declarations)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ element.SetAttribute("style", declarations + ";visibility:var(--a,hidden);--outside:var(--a,visible);display:block");
+ var style = element.ComputeCurrentStyle();
+ Assert.AreEqual("hidden", style.GetPropertyValue("visibility"));
+ Assert.AreEqual("visible", style.GetPropertyValue("--outside"));
+ Assert.AreEqual("block", style.GetPropertyValue("display"));
+
+ foreach (var name in new[] { "--a", "--b", "--c" })
+ {
+ Assert.AreEqual(String.Empty, style.GetPropertyValue(name), name);
+ }
+ }
+
+ [TestCase("var(--missing,hidden)", "hidden")]
+ [TestCase("var(--missing,var(--other,hidden))", "hidden")]
+ [TestCase("var(--missing,var(--other,var(--third,hidden)))", "hidden")]
+ [TestCase("var(--missing)", "visible")]
+ [TestCase("var(--missing,)", "visible")]
+ public void MissingVariablesAndNestedFallbacks(String value, String expected)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ element.SetAttribute("style", "visibility:" + value);
+ Assert.AreEqual(expected, element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [TestCase("--a:var(--a)", "var(--a)")]
+ [TestCase("--a:12px", "var(--a,visible)")]
+ [TestCase("--a:var(--missing,)", "var(--a,visible)")]
+ public void InvalidAtComputedValueTimeUsesInheritanceNotPreviousDeclaration(String custom, String value)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("span");
+ element.SetAttribute("style", custom + ";visibility:visible;visibility:" + value + ";width:10px;width:var(--missing)");
+ var style = element.ComputeCurrentStyle();
+ Assert.AreEqual("hidden", style.GetPropertyValue("visibility"));
+ Assert.AreEqual("auto", style.GetPropertyValue("width"));
+ }
+
+ [Test]
+ public void ResolvedTokensAreNotConvertedUsingTheCustomPropertyConverter()
+ {
+ using var document = ParseDocument("
");
+ var style = document.QuerySelector("div").ComputeCurrentStyle();
+ Assert.AreEqual("0", style.GetPropertyValue("--a"));
+ Assert.AreEqual("0", style.GetPropertyValue("opacity"));
+ Assert.AreEqual("0", style.GetPropertyValue("width"));
+ }
+
+ [TestCase("--a:red;--b:var(--a);--c:var(--a);--d:var(--b) var(--c)", "red red")]
+ [TestCase("--d:var(--missing,)", "")]
+ public void AcyclicDiamondsAndEmptyFallbacksRemainValid(String declarations, String expected)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ element.SetAttribute("style", declarations);
+ var style = element.ComputeCurrentStyle();
+ Assert.AreEqual(expected, style.GetPropertyValue("--d"));
+ Assert.IsNotInstanceOf(style.GetProperty("--d").RawValue);
+ }
+
+ [TestCase("--a:visible;--b:var(--a)", "--a:hidden", "visible")]
+ [TestCase("--a:var(--b);--b:var(--a)", "--a:visible", "hidden")]
+ [TestCase("--a:visible;--b:var(--a)", "--b:initial", "hidden")]
+ [TestCase("--a:visible;--b:var(--a)", "--a:hidden;--b:inherit", "visible")]
+ [TestCase("--a:visible;--b:var(--a)", "--a:hidden;--b:unset", "visible")]
+ public void InheritanceUsesTheParentsResolvedCustomValues(String parent, String child, String expected)
+ {
+ using var document = ParseDocument("
");
+ document.QuerySelector("div").SetAttribute("style", parent);
+ var element = document.QuerySelector("span");
+ element.SetAttribute("style", child + ";visibility:var(--b,hidden)");
+ Assert.AreEqual(expected, element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [TestCase("var(--b)", "hidden")]
+ [TestCase("hidden", "hidden")]
+ [TestCase("visible", "visible")]
+ public void SharedRulesAreStillLocalDeclarations(String childValue, String expected)
+ {
+ using var document = ParseDocument(
+ "");
+ var child = document.QuerySelector("#c");
+ var sibling = document.QuerySelector("#s");
+ var parent = document.QuerySelector("#p");
+ var styles = document.DefaultView.GetStyleCollection(new DefaultRenderDevice());
+ var parentStyle = styles.ComputeDeclarations(parent);
+
+ Assert.AreEqual(expected, child.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual(expected, styles.ComputeDeclarationsWithParent(child, parentStyle).GetPropertyValue("visibility"));
+ var rendered = RenderTreeBuilder.GetInstance(document.DefaultView).RenderElement(parent, styles.Device);
+ var renderedChild = rendered.Children.OfType().Single(node => node.Ref == child);
+ Assert.AreEqual(expected, renderedChild.ComputedStyle.GetPropertyValue("visibility"));
+ var cascade = styles.ComputeCascadedStyle(child, parentStyle);
+ Assert.AreEqual(expected, cascade.Compute(new CssComputeContext(styles.Device, document.Context, cascade, parentStyle)).GetPropertyValue("visibility"));
+ Assert.AreEqual("visible", sibling.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual("visible", parent.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual(expected, child.ComputeCurrentStyle().GetPropertyValue("visibility"));
+
+ using var inlineDocument = ParseDocument("");
+ Assert.AreEqual(expected, inlineDocument.QuerySelector("div div").ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [Test]
+ public void InheritedOrdinaryValuesAreNotRecomputedAgainstChildVariables()
+ {
+ using var document = ParseDocument("
");
+ Assert.AreEqual("hidden", document.QuerySelector("span").ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [TestCase("initial", "visible")]
+ [TestCase("inherit", "hidden")]
+ [TestCase("unset", "hidden")]
+ public void SubstitutedCssWideKeywordsAreAppliedToConsumers(String keyword, String expected)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("span");
+ var styles = document.DefaultView.GetStyleCollection(new DefaultRenderDevice());
+ var computed = element.ComputeCurrentStyle();
+ Assert.AreEqual(expected, computed.GetPropertyValue("visibility"));
+ Assert.AreEqual(keyword, computed.GetPropertyValue("--a"));
+ Assert.AreEqual(keyword, computed.Compute(new CssComputeContext(styles.Device, document.Context, computed)).GetPropertyValue("--a"));
+ }
+
+ [TestCase("--a:var(--a);--a:visible", "visible")]
+ [TestCase("--a:var(--a)!important;--a:visible", "hidden")]
+ [TestCase("--a:visible;--a:var(--a)", "hidden")]
+ public void OnlyTheWinningDeclarationParticipatesInTheGraph(String text, String expected)
+ {
+ using var document = ParseDocument("
");
+ Assert.AreEqual(expected, document.QuerySelector("div").ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [TestCase("--a:var(--a);margin:var(--a)", "0", "0")]
+ [TestCase("--a:var(--a);margin:var(--a,1px 2px)", "1px", "2px")]
+ [TestCase("--a:1px 2px;margin:var(--a)", "1px", "2px")]
+ [TestCase("--a:var(--a);margin:3px var(--a,4px)", "3px", "4px")]
+ public void ShorthandsUseTheCompleteSubstitutedValue(String text, String top, String right)
+ {
+ using var document = ParseDocument("
");
+ var style = document.QuerySelector("div").ComputeCurrentStyle();
+ Assert.AreEqual(top, style.GetPropertyValue("margin-top"));
+ Assert.AreEqual(right, style.GetPropertyValue("margin-right"));
+ Assert.AreEqual(top, style.GetPropertyValue("margin-bottom"));
+ Assert.AreEqual(right, style.GetPropertyValue("margin-left"));
+ }
+
+ [TestCase("'var(--a)'")]
+ [TestCase("\"var(--a)\"")]
+ [TestCase("url('var(--a)')")]
+ [TestCase("visible /*var(--a)*/")]
+ [TestCase("myvar(--a)")]
+ public void LiteralVariableTextDoesNotCreateDependencies(String text)
+ {
+ var value = new CssVariableValue(text);
+ Assert.IsEmpty(value.Dependencies);
+ Assert.AreEqual(text, value.Substitute(_ => null));
+ }
+
+ [TestCase(@"var(--\61)", "--a")]
+ [TestCase(@"v\61 r(--a)", "--a")]
+ [TestCase("VAR(--A)", "--A")]
+ [TestCase("var(/*comment*/--a)", "--a")]
+ public void DependenciesUseDecodedCaseSensitiveNames(String text, String name)
+ {
+ var value = new CssVariableValue(text);
+ Assert.AreEqual(new[] { name }, value.Dependencies.ToArray());
+ Assert.AreEqual("red", value.Substitute(n => n == name ? new CssAnyValue("red") : null));
+ }
+
+ [Test]
+ public void CustomNamesAreCaseSensitiveThroughoutCssomAndComputation()
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ Assert.AreEqual("hidden", element.GetStyle().GetPropertyValue("--a"));
+ Assert.AreEqual("visible", element.GetStyle().GetPropertyValue("--A"));
+ Assert.AreEqual("visible", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ element.GetStyle().RemoveProperty("--A");
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("--a"));
+ Assert.AreEqual(String.Empty, element.ComputeCurrentStyle().GetPropertyValue("--A"));
+ }
+
+ [Test]
+ public void MutationAndPriorityDoNotChangeSharedDeclarationObjects()
+ {
+ using var document = ParseDocument("" +
+ "
");
+ var element = document.QuerySelector("#a");
+ var other = document.QuerySelector("#b");
+ var sheet = (ICssStyleSheet)document.GetStyleSheets().Single();
+ var source = sheet.Rules[0].CssText;
+ var inline = element.GetStyle().CssText;
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual(source, sheet.Rules[0].CssText);
+ Assert.AreEqual(inline, element.GetStyle().CssText);
+ element.GetStyle().SetProperty("--b", "visible");
+ Assert.AreEqual("visible", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual("hidden", other.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual(source, sheet.Rules[0].CssText);
+ element.GetStyle().RemoveProperty("--b");
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [Test]
+ public void SubstitutionPreservesSurroundingTokensAndTokenBoundaries()
+ {
+ using var document = ParseDocument("
");
+ var style = document.QuerySelector("div").ComputeCurrentStyle();
+ Assert.AreEqual("rgba(255, 0, 0, 1)", style.GetPropertyValue("color"));
+ Assert.AreEqual("auto", style.GetPropertyValue("width"));
+ }
+
+ [Test]
+ public void MatchingIsReusedAtEachInheritanceBoundary()
+ {
+ using var document = ParseDocument("
");
+ var styles = new CountingStyleCollection(document.DefaultView.GetStyleCollection(new DefaultRenderDevice()));
+ var element = document.QuerySelector("span");
+ styles.ComputeDeclarations(element);
+ Assert.AreEqual(element.GetAncestors().OfType().Count() + 1, styles.Enumerations);
+ var parent = styles.ComputeDeclarations(element.ParentElement);
+ var before = styles.Enumerations;
+ styles.ComputeDeclarationsWithParent(element, parent);
+ Assert.AreEqual(before + 1, styles.Enumerations);
+ }
+
+ [Test]
+ public void ComponentDetectionAgreesWithReachability()
+ {
+ const Int32 count = 16;
+ var random = new Random(241);
+
+ for (var sample = 0; sample < 100; sample++)
+ {
+ var reachable = new Boolean[count, count];
+ var text = new StringBuilder();
+
+ for (var i = 0; i < count; i++)
+ {
+ text.Append("--v").Append(i).Append(':');
+
+ if (random.Next(3) == 0)
+ {
+ text.Append("red;");
+ }
+ else
+ {
+ var first = random.Next(count);
+ var second = random.Next(count);
+ reachable[i, first] = reachable[i, second] = true;
+ text.Append("var(--v").Append(first).Append(",var(--v").Append(second).Append(",red));");
+ }
+ }
+
+ for (var k = 0; k < count; k++)
+ {
+ for (var i = 0; i < count; i++)
+ {
+ for (var j = 0; j < count; j++)
+ {
+ reachable[i, j] |= reachable[i, k] && reachable[k, j];
+ }
+ }
+ }
+
+ var resolver = new CssCustomPropertyResolver(ParseDeclarations(text.ToString()));
+
+ for (var i = 0; i < count; i++)
+ {
+ var value = resolver.Resolve("--v" + i);
+ Assert.AreEqual(reachable[i, i], value is null, "Sample {0}, variable {1}", sample, i);
+
+ if (value is not null)
+ {
+ Assert.AreEqual("red", value.CssText.Replace("/**/", String.Empty));
+ }
+ }
+ }
+ }
+
+ [Test]
+ public void SubstitutionLimitIncludesTheBoundary()
+ {
+ var variable = new CssVariableValue("var(--a)");
+ var maximum = new String('x', CssVariableValue.MaxSubstitutionLength);
+ Assert.AreEqual(maximum, variable.Substitute(_ => new CssAnyValue(maximum)));
+ Assert.IsNull(variable.Substitute(_ => new CssAnyValue(maximum + "x")));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void LongNamedChainsAndCyclesUseBoundedStackSpace(Boolean cycle)
+ {
+ const Int32 count = 4096;
+ var text = new StringBuilder();
+
+ for (var i = 0; i < count - 1; i++)
+ {
+ text.Append("--v").Append(i).Append(":var(--v").Append(i + 1).Append(");");
+ }
+
+ text.Append("--v").Append(count - 1).Append(cycle ? ":var(--v0);" : ":visible;");
+ text.Append("visibility:var(--v0,hidden)");
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ element.SetAttribute("style", text.ToString());
+ Assert.AreEqual(cycle ? "hidden" : "visible", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void DeepFallbacksUseBoundedStackSpace(Boolean rawFallback)
+ {
+ const Int32 count = 8192;
+ var prefix = rawFallback ? "var(--missing,calc(" : "var(--missing,";
+ var suffix = rawFallback ? "))" : ")";
+ var text = String.Concat(Enumerable.Repeat(prefix, count)) + "red" + String.Concat(Enumerable.Repeat(suffix, count));
+ using var document = ParseDocument(" ");
+ var element = document.QuerySelector("span");
+ element.GetStyle().SetProperty("--a", text);
+ element.GetStyle().SetProperty("color", "var(--a,blue)");
+ Assert.IsNotNull(element.GetStyle().GetProperty("--a").RawValue);
+ var style = element.ComputeCurrentStyle();
+ Assert.IsNotNull(style);
+
+ if (!rawFallback)
+ {
+ Assert.AreEqual("rgba(255, 0, 0, 1)", style.GetPropertyValue("color"));
+ }
+ }
+
+ [Test]
+ public void ExponentialSubstitutionIsBounded()
+ {
+ var text = new StringBuilder("--v0:red;");
+
+ for (var i = 1; i < 24; i++)
+ {
+ text.Append("--v").Append(i).Append(":var(--v").Append(i - 1).Append(") var(--v").Append(i - 1).Append(");");
+ }
+
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ element.SetAttribute("style", text + "visibility:var(--v23,hidden)");
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ private sealed class CountingStyleCollection : IStyleCollection
+ {
+ private readonly IStyleCollection _inner;
+
+ public CountingStyleCollection(IStyleCollection inner) => _inner = inner;
+
+ public IRenderDevice Device => _inner.Device;
+
+ public Int32 Enumerations { get; private set; }
+
+ public IEnumerator GetEnumerator()
+ {
+ Enumerations++;
+ return _inner.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
index e199e267..c9128d86 100644
--- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
+++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
@@ -111,7 +111,9 @@ public Boolean IsImportant
public ICssProperty Compute(ICssComputeContext context)
{
var propertyContext = new PropertyComputeContext(context, _converter);
- var computedValue = _value?.Compute(propertyContext);
+ var computedValue = _name.StartsWith("--", StringComparison.Ordinal) ?
+ context.Resolve(_name) ?? CssInvalidValue.Instance :
+ _value is CssChildValue child ? child.Compute(propertyContext, _name) : _value?.Compute(propertyContext);
if (computedValue != _value)
{
diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs
index 1163a282..bc44cbb0 100644
--- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs
+++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs
@@ -37,7 +37,7 @@ sealed class CssStyleDeclaration : ICssStyleDeclaration
public CssStyleDeclaration(IBrowsingContext context)
{
_declarations = new List();
- _declarationIndex = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _declarationIndex = new Dictionary(StringComparer.Ordinal);
_context = context;
}
@@ -79,11 +79,13 @@ public String CssText
public ICssProperty GetProperty(String name)
{
+ name = name.StartsWith("--", StringComparison.Ordinal) ? name : name.ToLowerFast();
+
if (_declarationIndex.TryGetValue(name, out var index) && index < _declarations.Count)
{
var declaration = _declarations[index];
- if (declaration.Name.Isi(name))
+ if (declaration.Name.Is(name))
{
return declaration;
}
@@ -391,6 +393,7 @@ private void SetProperty(ICssProperty property)
private void RemovePropertyByName(String propertyName)
{
+ propertyName = propertyName.StartsWith("--", StringComparison.Ordinal) ? propertyName : propertyName.ToLowerFast();
var info = _context.GetDeclarationInfo(propertyName);
var longhands = info.Longhands;
diff --git a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
index c2f05b58..e4882f60 100644
--- a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
@@ -1,6 +1,7 @@
#nullable disable
namespace AngleSharp.Css.Dom
{
+ using AngleSharp.Css.Converters;
using AngleSharp.Css.Parser;
using AngleSharp.Css.Values;
using AngleSharp.Dom;
@@ -92,10 +93,40 @@ public static ICssStyleDeclaration Compute(this ICssStyleDeclaration style, ICss
foreach (var property in style)
{
- computedStyle.AddProperty(property.Compute(context));
+ var computed = property.Compute(context);
+
+ var substitutedKeyword = property.RawValue is not ICssSpecialValue && computed.RawValue is ICssSpecialValue;
+
+ if ((computed.RawValue is null || substitutedKeyword) && property.RawValue is not null && property is CssProperty cssProperty)
+ {
+ var inherit = computed.RawValue is CssInheritValue ||
+ (computed.RawValue is not CssInitialValue && property.CanBeInherited);
+ var inherited = inherit && context is CssComputeContext cssContext ?
+ cssContext.InheritedValue(property.Name) : null;
+ var initial = context.Context.GetDeclarationInfo(property.Name).InitialValue;
+ var value = inherited ?? (initial is null ? null : cssProperty.Converter.Convert(initial.CssText)?.Compute(context));
+ computed = new CssProperty(property.Name, cssProperty.Converter, cssProperty.Flags, value, property.IsImportant);
+ }
+
+ computedStyle.AddProperty(computed);
}
return computedStyle;
}
+
+ internal static CssStyleDeclaration Cascade(this ICssStyleDeclaration style, ICssStyleDeclaration parent, ICssComputeContext context)
+ {
+ var declarations = new CssStyleDeclaration(context.Context);
+
+ // Resolve local custom declarations before merging the parent. In
+ // particular, initial must not disappear through IsInherited.
+ foreach (var property in style)
+ {
+ declarations.AddProperty(property.Name.StartsWith("--", StringComparison.Ordinal) ? property.Compute(context) : property);
+ }
+
+ declarations.UpdateDeclarations(parent);
+ return declarations;
+ }
}
}
diff --git a/src/AngleSharp.Css/Extensions/DeclarationInfoExtensions.cs b/src/AngleSharp.Css/Extensions/DeclarationInfoExtensions.cs
index e2a7a549..a61f2257 100644
--- a/src/AngleSharp.Css/Extensions/DeclarationInfoExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/DeclarationInfoExtensions.cs
@@ -56,7 +56,7 @@ public static IEnumerable NotNull(this IEnumerable enumerable)
if (value is ICssRawValue || value is CssChildValue)
{
- var child = new CssChildValue(value);
+ var child = new CssChildValue(value, shorthandName: info.Name);
return Enumerable
.Repeat(child, longhands.Length)
.ToArray();
diff --git a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
index de94d76f..408ff3d5 100644
--- a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
@@ -44,13 +44,7 @@ public static IStyleCollection GetStyleCollection(this IWindow window, IRenderDe
/// The optional pseudo selector to use.
/// The style declaration containing all the declarations.
public static ICssStyleDeclaration ComputeDeclarations(this IStyleCollection styles, IElement element, String? pseudoSelector = null)
- {
- var ctx = element.Owner?.Context;
- var declarations = GetDeclarations(styles, element, pseudoSelector);
- var context = new CssComputeContext(styles.Device, ctx, declarations);
-
- return declarations.Compute(context);
- }
+ => GetElementDeclarations(styles, element, pseudoSelector, true);
///
/// Gets the declarations for the given element in the context of
@@ -61,29 +55,40 @@ public static ICssStyleDeclaration ComputeDeclarations(this IStyleCollection sty
/// The optional pseudo selector to use.
/// The style declaration containing all the declarations.
public static ICssStyleDeclaration GetDeclarations(this IStyleCollection styles, IElement element, String? pseudoSelector = null)
+ => GetElementDeclarations(styles, element, pseudoSelector, false);
+
+ private static ICssStyleDeclaration GetElementDeclarations(IStyleCollection styles, IElement element, String? pseudoSelector, Boolean compute)
{
var ctx = element.Owner?.Context;
- var computedStyle = new CssStyleDeclaration(ctx);
- var nodes = element.GetAncestors().OfType();
+ ICssStyleDeclaration? parent = null;
+ var nodes = new Stack();
if (!String.IsNullOrEmpty(pseudoSelector))
{
- var pseudoElement = element?.Pseudo(pseudoSelector!.TrimStart(':'));
+ var pseudoElement = element.Pseudo(pseudoSelector!.TrimStart(':'));
if (pseudoElement is not null)
{
- element = pseudoElement;
+ nodes.Push(pseudoElement);
}
}
- computedStyle.SetDeclarations(styles.ComputeExplicitStyle(element!));
+ nodes.Push(element);
- foreach (var node in nodes)
+ foreach (var ancestor in element.GetAncestors().OfType())
{
- computedStyle.UpdateDeclarations(styles.ComputeExplicitStyle(node));
+ nodes.Push(ancestor);
}
- return computedStyle;
+ while (nodes.Count > 0)
+ {
+ var explicitStyle = styles.ComputeExplicitStyle(nodes.Pop());
+ var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parent);
+ var declarations = explicitStyle.Cascade(parent!, context);
+ parent = compute ? declarations.Compute(context) : declarations;
+ }
+
+ return parent!;
}
///
@@ -96,9 +101,9 @@ public static ICssStyleDeclaration GetDeclarations(this IStyleCollection styles,
/// Returns the cascaded read-only style declaration.
public static ICssStyleDeclaration ComputeCascadedStyle(this IStyleCollection styles, IElement element, ICssStyleDeclaration parent)
{
- var computedStyle = (CssStyleDeclaration)styles.ComputeExplicitStyle(element);
- computedStyle.UpdateDeclarations(parent);
- return computedStyle;
+ var explicitStyle = styles.ComputeExplicitStyle(element);
+ var context = new CssComputeContext(styles.Device, element.Owner?.Context, explicitStyle, parent);
+ return explicitStyle.Cascade(parent, context);
}
///
@@ -140,18 +145,9 @@ public static ICssStyleDeclaration ComputeExplicitStyle(this IStyleCollection st
internal static ICssStyleDeclaration ComputeDeclarationsWithParent(this IStyleCollection styles, IElement element, ICssStyleDeclaration parentComputedStyle)
{
var ctx = element.Owner?.Context;
- var computedStyle = new CssStyleDeclaration(ctx);
-
- // Element's own cascaded style (CSS rule matching + inline style).
- computedStyle.SetDeclarations(styles.ComputeExplicitStyle(element));
-
- // Inherit from the parent's already-computed style instead of walking
- // all ancestors individually. The parent style already includes the
- // full ancestor inheritance chain.
- computedStyle.UpdateDeclarations(parentComputedStyle);
-
- var context = new CssComputeContext(styles.Device, ctx, computedStyle);
- return computedStyle.Compute(context);
+ var explicitStyle = styles.ComputeExplicitStyle(element);
+ var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parentComputedStyle);
+ return explicitStyle.Cascade(parentComputedStyle, context).Compute(context);
}
#endregion
diff --git a/src/AngleSharp.Css/Parser/CssTokenizer.cs b/src/AngleSharp.Css/Parser/CssTokenizer.cs
index 47cd672c..88b8d7b8 100644
--- a/src/AngleSharp.Css/Parser/CssTokenizer.cs
+++ b/src/AngleSharp.Css/Parser/CssTokenizer.cs
@@ -260,6 +260,10 @@ private CssToken Data(Char current)
Advance(2);
return NewCloseComment();
}
+ else if (c1 == Symbols.Minus)
+ {
+ return IdentStart(current);
+ }
}
else
{
@@ -706,7 +710,7 @@ private CssToken IdentStart(Char current)
{
current = GetNext();
- if (current.IsNameStart() || IsValidEscape(current))
+ if (current.IsNameStart() || current == Symbols.Minus || IsValidEscape(current))
{
StringBuffer.Append(Symbols.Minus);
return IdentRest(current);
diff --git a/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs b/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
index 72d269f6..ffd9cd63 100644
--- a/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
+++ b/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
@@ -5,7 +5,6 @@ namespace AngleSharp.Css.Parser
using AngleSharp.Css.Values;
using AngleSharp.Text;
using System;
- using System.Collections.Generic;
///
/// Represents extensions to for general CSS functions.
@@ -37,54 +36,14 @@ public static CssAttrValue ParseAttr(this StringSource source)
///
public static CssReferenceValue ParseVars(this StringSource source)
{
- var index = source.Index;
- var start = index;
- var length = FunctionNames.Var.Length;
- var refs = default(List>);
-
- while (!source.IsDone)
+ if (source.Content.IndexOf(FunctionNames.Var, StringComparison.OrdinalIgnoreCase) < 0 &&
+ source.Content.IndexOf('\\') < 0)
{
- index = source.Content.IndexOf(FunctionNames.Var, index, StringComparison.OrdinalIgnoreCase) + length;
-
- if (index >= length)
- {
- source.NextTo(index);
- var c = source.SkipSpacesAndComments();
-
- if (c == Symbols.RoundBracketOpen)
- {
- source.SkipCurrentAndSpaces();
- var s = new TextPosition(0, 0, source.Index);
- var reference = ParseVar(source);
-
- if (reference == null)
- {
- refs = null;
- break;
- }
-
- if (refs == null)
- {
- refs = new List>();
- }
-
- var e = new TextPosition(0, 0, source.Index);
- refs.Add(Tuple.Create(new TextRange(s, e), reference));
- continue;
- }
- }
-
- break;
+ return null;
}
- source.BackTo(start);
-
- if (refs != null)
- {
- return new CssReferenceValue(source.Content, refs);
- }
-
- return null;
+ var value = new CssVariableValue(source.Content);
+ return value.IsValid && value.HasReferences ? new CssReferenceValue(value) : null;
}
///
@@ -116,15 +75,9 @@ public static CssVarValue ParseVar(this StringSource source)
///
public static ICssValue ParseVarFallback(this StringSource source)
{
- if (!source.IsFunction(FunctionNames.Var))
- {
- var content = source.TakeUntilClosed();
- source.SkipCurrentAndSpaces();
- return new CssAnyValue(content);
- }
-
- return source.ParseVar();
-
+ var content = source.TakeUntilClosed();
+ source.SkipCurrentAndSpaces();
+ return new CssAnyValue(content);
}
///
diff --git a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
index cda0ef31..75e93e3a 100644
--- a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
+++ b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
@@ -84,24 +84,10 @@ private ElementRenderNode RenderElement(
{
var explicitStyle = collection.ComputeExplicitStyle(element);
- var specifiedStyle = new CssStyleDeclaration(_context);
- specifiedStyle.SetDeclarations(explicitStyle);
-
- if (parentSpecifiedStyle is not null)
- {
- specifiedStyle.UpdateDeclarations(parentSpecifiedStyle);
- }
-
- var computedDeclarations = new CssStyleDeclaration(_context);
- computedDeclarations.SetDeclarations(explicitStyle);
-
- if (parentComputedStyle is not null)
- {
- computedDeclarations.UpdateDeclarations(parentComputedStyle);
- }
-
- var computeContext = new CssComputeContext(collection.Device, _context, computedDeclarations);
- var computedStyle = computedDeclarations.Compute(computeContext);
+ var specifiedContext = new CssComputeContext(collection.Device, _context, explicitStyle, parentSpecifiedStyle);
+ var specifiedStyle = explicitStyle.Cascade(parentSpecifiedStyle!, specifiedContext);
+ var computeContext = new CssComputeContext(collection.Device, _context, explicitStyle, parentComputedStyle);
+ var computedStyle = explicitStyle.Cascade(parentComputedStyle!, computeContext).Compute(computeContext);
var children = new List();
var node = new ElementRenderNode(element, parent, children, specifiedStyle, computedStyle);
diff --git a/src/AngleSharp.Css/Values/CssChildValue.cs b/src/AngleSharp.Css/Values/CssChildValue.cs
index 7aeda032..3919fca8 100644
--- a/src/AngleSharp.Css/Values/CssChildValue.cs
+++ b/src/AngleSharp.Css/Values/CssChildValue.cs
@@ -2,6 +2,7 @@
namespace AngleSharp.Css.Values
{
using AngleSharp.Css.Dom;
+ using AngleSharp.Css.Parser;
using System;
using System.Collections.Generic;
@@ -14,6 +15,7 @@ sealed class CssChildValue : ICssValue, IEquatable
private readonly ICssValue _parent;
private readonly ICssValue _value;
+ private readonly String _shorthandName;
#endregion
@@ -24,10 +26,12 @@ sealed class CssChildValue : ICssValue, IEquatable
///
/// The reference to the shorthand value.
/// The value of the child, if any.
- public CssChildValue(ICssValue parent, ICssValue value = null)
+ /// The shorthand that supplied the pending value.
+ public CssChildValue(ICssValue parent, ICssValue value = null, String shorthandName = null)
{
_parent = parent;
_value = value;
+ _shorthandName = shorthandName;
}
#endregion
@@ -73,7 +77,38 @@ ICssValue ICssValue.Compute(ICssComputeContext context)
{
var parent = _parent.Compute(context);
var value = _value?.Compute(context);
- return new CssChildValue(parent, value);
+ return new CssChildValue(parent, value, _shorthandName);
+ }
+
+ internal ICssValue Compute(ICssComputeContext context, String longhandName)
+ {
+ var parent = _parent;
+ var shorthandName = _shorthandName;
+
+ while (parent is CssChildValue child)
+ {
+ shorthandName = child._shorthandName;
+ parent = child.Parent;
+ }
+
+ if (shorthandName is not null && parent is ICssRawValue)
+ {
+ var text = new CssVariableValue(parent.CssText).Substitute(context.Resolve);
+
+ if (text is null)
+ {
+ return null;
+ }
+
+ // Parse the substituted shorthand once its complete token stream
+ // is known, rather than feeding it to an individual longhand's
+ // converter and discarding the remaining components.
+ var parser = context.Context?.GetService() ?? new CssParser(context.Context);
+ var declarations = parser.ParseDeclaration(shorthandName + ":" + text);
+ return declarations.GetProperty(longhandName)?.RawValue?.Compute(context);
+ }
+
+ return ((ICssValue)this).Compute(context);
}
Boolean IEquatable.Equals(ICssValue other) => other is CssChildValue value && Equals(value);
diff --git a/src/AngleSharp.Css/Values/CssComputeContext.cs b/src/AngleSharp.Css/Values/CssComputeContext.cs
index 09055071..d2e7255f 100644
--- a/src/AngleSharp.Css/Values/CssComputeContext.cs
+++ b/src/AngleSharp.Css/Values/CssComputeContext.cs
@@ -8,13 +8,15 @@ sealed class CssComputeContext : ICssComputeContext
{
private readonly IRenderDevice _device;
private readonly IBrowsingContext? _context;
- private readonly ICssProperties _properties;
+ private readonly CssCustomPropertyResolver _variables;
+ private readonly ICssProperties? _parent;
- public CssComputeContext(IRenderDevice device, IBrowsingContext? context, ICssProperties properties)
+ public CssComputeContext(IRenderDevice device, IBrowsingContext? context, ICssProperties properties, ICssProperties? parent = null)
{
_device = device ?? new DefaultRenderDevice();
_context = context;
- _properties = properties;
+ _variables = new CssCustomPropertyResolver(properties, parent);
+ _parent = parent;
}
public IRenderDevice Device => _device;
@@ -23,16 +25,9 @@ public CssComputeContext(IRenderDevice device, IBrowsingContext? context, ICssPr
public IValueConverter? Converter => null;
- public ICssValue? Resolve(String name)
- {
- if (name.StartsWith("--"))
- {
- var property = _properties.FirstOrDefault(m => m.Name.Equals(name, StringComparison.Ordinal));
- return property?.RawValue;
- }
+ public ICssValue? Resolve(String name) => _variables.Resolve(name);
- return null;
- }
+ internal ICssValue? InheritedValue(String name) => _parent?.FirstOrDefault(m => m.Name == name)?.RawValue;
}
}
diff --git a/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs b/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
new file mode 100644
index 00000000..56ff1102
--- /dev/null
+++ b/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
@@ -0,0 +1,161 @@
+namespace AngleSharp.Css.Values
+{
+ using AngleSharp.Css.Dom;
+ using System;
+ using System.Collections.Generic;
+
+ sealed class CssCustomPropertyResolver
+ {
+ private readonly Dictionary _values = new(StringComparer.Ordinal);
+
+ public CssCustomPropertyResolver(IEnumerable properties, ICssProperties? parent = null)
+ {
+ if (parent is not null)
+ {
+ foreach (var property in parent)
+ {
+ if (property.Name.StartsWith("--", StringComparison.Ordinal))
+ {
+ _values[property.Name] = property.RawValue is CssInvalidValue ? null : property.RawValue;
+ }
+ }
+ }
+
+ var nodes = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var property in properties)
+ {
+ if (property.Name.StartsWith("--", StringComparison.Ordinal))
+ {
+ var value = property.RawValue;
+
+ if (value is CssAnyValue { IsResolved: true })
+ {
+ _values[property.Name] = value;
+ continue;
+ }
+
+ var tokens = value is null || value is CssInvalidValue ? null : new CssVariableValue(value.CssText);
+ var keyword = tokens?.Keyword;
+
+ if (String.Equals(keyword, CssKeywords.Inherit, StringComparison.OrdinalIgnoreCase) ||
+ String.Equals(keyword, CssKeywords.Unset, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ _values[property.Name] = null;
+
+ if (tokens is not null && tokens.IsValid && !String.Equals(keyword, CssKeywords.Initial, StringComparison.OrdinalIgnoreCase))
+ {
+ nodes[property.Name] = new Node(property.Name, tokens);
+ }
+ }
+ }
+
+ foreach (var node in nodes.Values)
+ {
+ foreach (var name in node.Value.Dependencies)
+ {
+ if (nodes.TryGetValue(name, out var dependency))
+ {
+ node.Dependencies.Add(dependency);
+ }
+ }
+ }
+
+ // Iterative Tarjan traversal: complete components in dependency order.
+ // All fallback edges participate, even if substitution won't use them.
+ var index = 0;
+ var active = new Stack();
+ var visits = new Stack();
+ var component = new List();
+
+ foreach (var root in nodes.Values)
+ {
+ if (root.Index >= 0)
+ {
+ continue;
+ }
+
+ Enter(root);
+
+ while (visits.Count > 0)
+ {
+ var node = visits.Peek();
+
+ if (node.NextDependency < node.Dependencies.Count)
+ {
+ var dependency = node.Dependencies[node.NextDependency++];
+
+ if (dependency.Index < 0)
+ {
+ Enter(dependency);
+ }
+ else if (dependency.Active)
+ {
+ node.LowLink = Math.Min(node.LowLink, dependency.Index);
+ }
+
+ continue;
+ }
+
+ visits.Pop();
+
+ if (visits.Count > 0)
+ {
+ var previous = visits.Peek();
+ previous.LowLink = Math.Min(previous.LowLink, node.LowLink);
+ }
+
+ if (node.LowLink == node.Index)
+ {
+ component.Clear();
+ Node member;
+
+ do
+ {
+ member = active.Pop();
+ member.Active = false;
+ component.Add(member);
+ }
+ while (member != node);
+
+ if (component.Count == 1 && !node.Dependencies.Contains(node))
+ {
+ var text = node.Value.Substitute(Resolve);
+ _values[node.Name] = text is null ? null : new CssAnyValue(text, isResolved: true);
+ }
+ }
+ }
+ }
+
+ void Enter(Node node)
+ {
+ node.Index = node.LowLink = index++;
+ node.Active = true;
+ active.Push(node);
+ visits.Push(node);
+ }
+ }
+
+ public ICssValue? Resolve(String name) => _values.TryGetValue(name, out var value) ? value : null;
+
+ private sealed class Node
+ {
+ public Node(String name, CssVariableValue value)
+ {
+ Name = name;
+ Value = value;
+ }
+
+ public String Name { get; }
+ public CssVariableValue Value { get; }
+ public List Dependencies { get; } = new();
+ public Int32 Index { get; set; } = -1;
+ public Int32 LowLink { get; set; }
+ public Int32 NextDependency { get; set; }
+ public Boolean Active { get; set; }
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Values/CssVariableValue.cs b/src/AngleSharp.Css/Values/CssVariableValue.cs
new file mode 100644
index 00000000..3cd2eba0
--- /dev/null
+++ b/src/AngleSharp.Css/Values/CssVariableValue.cs
@@ -0,0 +1,279 @@
+namespace AngleSharp.Css.Values
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Css.Parser;
+ using AngleSharp.Css.Parser.Tokens;
+ using AngleSharp.Text;
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ // A flat token stream keeps both dependency discovery and nested fallback
+ // substitution off the CLR stack. Strings, URLs and comments are opaque.
+ sealed class CssVariableValue
+ {
+ // Bound expansion of small, exponentially growing variable definitions.
+ internal const Int32 MaxSubstitutionLength = 1024 * 1024;
+
+ private readonly List _tokens = new();
+ private readonly Dictionary _references = new();
+
+ public CssVariableValue(String text)
+ {
+ Text = text;
+ var tokenizer = new CssTokenizer(new TextSource(text));
+ var blocks = new Stack();
+ var ends = new Dictionary();
+
+ while (true)
+ {
+ var token = tokenizer.Get();
+
+ if (token.Type == CssTokenType.EndOfFile)
+ {
+ break;
+ }
+
+ var index = _tokens.Count;
+ _tokens.Add(token);
+
+ if (IsOpen(token.Type))
+ {
+ blocks.Push(index);
+ }
+ else if (IsClose(token.Type) && blocks.Count > 0)
+ {
+ var start = blocks.Pop();
+ IsValid &= Matches(_tokens[start].Type, token.Type);
+ ends[start] = index;
+ }
+ }
+
+ // CSS syntax closes outstanding blocks at EOF. Keep the original
+ // text for CSSOM serialization, including incomplete URL strings.
+ while (blocks.Count > 0)
+ {
+ var start = blocks.Pop();
+ ends[start] = _tokens.Count;
+ _tokens.Add(new CssToken(CloseType(_tokens[start].Type), String.Empty)
+ {
+ Position = new TextPosition(0, 0, text.Length + 1),
+ });
+ }
+
+ for (var i = 0; i < _tokens.Count; i++)
+ {
+ var token = _tokens[i];
+
+ if (token.Type == CssTokenType.Function && token.Data.Equals(FunctionNames.Var, StringComparison.OrdinalIgnoreCase))
+ {
+ var name = SkipTrivia(i + 1);
+ var separator = SkipTrivia(name + 1);
+ var valid = ends.TryGetValue(i, out var end) &&
+ name < end && _tokens[name].Type == CssTokenType.Ident &&
+ _tokens[name].Data.StartsWith("--", StringComparison.Ordinal) &&
+ _tokens[name].Data.Length > 2 &&
+ (separator == end || _tokens[separator].Type == CssTokenType.Comma);
+
+ IsValid &= valid;
+
+ if (valid)
+ {
+ _references.Add(i, new Reference(_tokens[name].Data, name, end, separator < end ? separator + 1 : -1));
+ }
+ }
+ }
+ }
+
+ public String Text { get; }
+
+ public Boolean IsValid { get; } = true;
+
+ public Boolean HasReferences => _references.Count > 0;
+
+ public IEnumerable Dependencies
+ {
+ get
+ {
+ foreach (var reference in _references.Values)
+ {
+ yield return reference.Name;
+ }
+ }
+ }
+
+ public String? Keyword
+ {
+ get
+ {
+ var index = SkipTrivia(0);
+ return index < _tokens.Count && _tokens[index].Type == CssTokenType.Ident &&
+ SkipTrivia(index + 1) == _tokens.Count ? _tokens[index].Data : null;
+ }
+ }
+
+ public IEnumerable> GetReferences()
+ {
+ for (var i = 0; i < _tokens.Count; i++)
+ {
+ if (_references.TryGetValue(i, out var reference))
+ {
+ var fallback = reference.Fallback >= 0 ?
+ new CssAnyValue(Text.Substring(Offset(reference.Fallback - 1) + 1,
+ Offset(reference.End) - Offset(reference.Fallback - 1) - 1).Trim()) : null;
+ var start = new TextPosition(0, 0, Offset(reference.NameIndex));
+ var end = new TextPosition(0, 0, EndOffset(reference.End));
+ yield return Tuple.Create(new TextRange(start, end), new CssVarValue(reference.Name, fallback));
+ i = reference.End;
+ }
+ }
+ }
+
+ public String? Substitute(Func resolve)
+ {
+ if (!IsValid)
+ {
+ return null;
+ }
+
+ if (!HasReferences)
+ {
+ return Text;
+ }
+
+ var result = new StringBuilder();
+ var fallbacks = new Stack();
+ var cursor = 0;
+
+ for (var i = 0; i < _tokens.Count; i++)
+ {
+ if (fallbacks.Count > 0 && fallbacks.Peek() == i)
+ {
+ if (!Append(result, cursor, Offset(i)) || !Separate(result, NeedsSeparator(EndOffset(i))))
+ {
+ return null;
+ }
+
+ cursor = EndOffset(i);
+ fallbacks.Pop();
+ }
+ else if (_references.TryGetValue(i, out var reference))
+ {
+ if (!Append(result, cursor, Offset(i)) || !Separate(result, result.Length > 0))
+ {
+ return null;
+ }
+
+ var value = resolve(reference.Name);
+
+ if (value is not null)
+ {
+ var text = value.CssText;
+
+ if (text.Length > MaxSubstitutionLength - result.Length)
+ {
+ return null;
+ }
+
+ result.Append(text);
+ cursor = EndOffset(reference.End);
+ i = reference.End;
+
+ if (!Separate(result, NeedsSeparator(cursor)))
+ {
+ return null;
+ }
+ }
+ else if (reference.Fallback >= 0)
+ {
+ cursor = Offset(reference.Fallback - 1) + 1;
+ i = reference.Fallback - 1;
+ fallbacks.Push(reference.End);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ return Append(result, cursor, Text.Length) ? result.ToString().Trim() : null;
+ }
+
+ private Boolean Append(StringBuilder result, Int32 start, Int32 end)
+ {
+ var length = end - start;
+
+ if (length > MaxSubstitutionLength - result.Length)
+ {
+ return false;
+ }
+
+ result.Append(Text, start, length);
+ return true;
+ }
+
+ private static Boolean Separate(StringBuilder result, Boolean needed)
+ {
+ if (needed && result.Length > 0 && !result[result.Length - 1].IsSpaceCharacter())
+ {
+ if (result.Length > MaxSubstitutionLength - 4)
+ {
+ return false;
+ }
+
+ // Substitution must not turn adjacent tokens into a new token
+ // (for example, var(--number)px must not become a dimension).
+ result.Append("/**/");
+ }
+
+ return true;
+ }
+
+ private Int32 Offset(Int32 index) => _tokens[index].Position.Position - 1;
+
+ private Int32 EndOffset(Int32 index) => Math.Min(Offset(index) + 1, Text.Length);
+
+ private Boolean NeedsSeparator(Int32 index) => index < Text.Length && !Text[index].IsSpaceCharacter();
+
+ private Int32 SkipTrivia(Int32 index)
+ {
+ while (index < _tokens.Count && (_tokens[index].Type == CssTokenType.Whitespace || _tokens[index].Type == CssTokenType.Comment))
+ {
+ index++;
+ }
+
+ return index;
+ }
+
+ private static Boolean IsOpen(CssTokenType type) =>
+ type == CssTokenType.Function || type == CssTokenType.RoundBracketOpen ||
+ type == CssTokenType.SquareBracketOpen || type == CssTokenType.CurlyBracketOpen;
+
+ private static Boolean IsClose(CssTokenType type) =>
+ type == CssTokenType.RoundBracketClose || type == CssTokenType.SquareBracketClose ||
+ type == CssTokenType.CurlyBracketClose;
+
+ private static CssTokenType CloseType(CssTokenType type) =>
+ type == CssTokenType.SquareBracketOpen ? CssTokenType.SquareBracketClose :
+ type == CssTokenType.CurlyBracketOpen ? CssTokenType.CurlyBracketClose : CssTokenType.RoundBracketClose;
+
+ private static Boolean Matches(CssTokenType open, CssTokenType close) => CloseType(open) == close;
+
+ private readonly struct Reference
+ {
+ public Reference(String name, Int32 nameIndex, Int32 end, Int32 fallback)
+ {
+ Name = name;
+ NameIndex = nameIndex;
+ End = end;
+ Fallback = fallback;
+ }
+
+ public String Name { get; }
+ public Int32 NameIndex { get; }
+ public Int32 End { get; }
+ public Int32 Fallback { get; }
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Values/Functions/CssVarValue.cs b/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
index 957fedf5..d9d4167d 100644
--- a/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
+++ b/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
@@ -121,14 +121,20 @@ public Boolean Equals(CssVarValue other)
/// The resolved value or null.
public ICssValue Compute(ICssComputeContext context)
{
- var value = context.Resolve(_variableName)?.Compute(context);
+ var value = context.Resolve(_variableName);
if (value is not null)
{
- return value;
+ return value.Compute(context);
}
- return _defaultValue?.Compute(context);
+ if (_defaultValue is null)
+ {
+ return null;
+ }
+
+ var text = new CssVariableValue(_defaultValue.CssText).Substitute(context.Resolve);
+ return text is null ? null : ((ICssValue)new CssAnyValue(text)).Compute(context);
}
Boolean IEquatable.Equals(ICssValue other) => other is CssVarValue value && Equals(value);
diff --git a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
index f942f97c..5b3fa40c 100644
--- a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
+++ b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
@@ -3,6 +3,8 @@ namespace AngleSharp.Css.Values
{
using AngleSharp.Css.Converters;
using AngleSharp.Css.Dom;
+ using AngleSharp.Css.Parser;
+ using AngleSharp.Text;
using System;
///
@@ -22,11 +24,15 @@ sealed class CssAnyValue : ICssRawValue
/// Creates a new unknown value with the given literal content.
///
/// The serialized value representation..
- public CssAnyValue(String text)
+ /// Whether variable substitution has already been performed.
+ public CssAnyValue(String text, Boolean isResolved = false)
{
_text = text;
+ IsResolved = isResolved;
}
+ internal Boolean IsResolved { get; }
+
#endregion
#region Properties
@@ -51,11 +57,14 @@ ICssValue ICssValue.Compute(ICssComputeContext context)
if (converter is not null && converter is not AnyValueConverter)
{
- var value = converter.Convert(_text);
- return value?.Compute(context);
+ var source = new StringSource(_text);
+ source.SkipSpacesAndComments();
+ var value = converter.Convert(source);
+ source.SkipSpacesAndComments();
+ return source.IsDone ? value?.Compute(context) : null;
}
- return null;
+ return this;
}
Boolean IEquatable.Equals(ICssValue other) => other is CssAnyValue o && _text == o.CssText;
diff --git a/src/AngleSharp.Css/Values/Raws/CssInvalidValue.cs b/src/AngleSharp.Css/Values/Raws/CssInvalidValue.cs
new file mode 100644
index 00000000..0f5a2705
--- /dev/null
+++ b/src/AngleSharp.Css/Values/Raws/CssInvalidValue.cs
@@ -0,0 +1,22 @@
+namespace AngleSharp.Css.Values
+{
+ using AngleSharp.Css.Dom;
+ using System;
+
+ // Unlike a missing declaration, guaranteed-invalid is inherited as-is and
+ // cannot be repaired by resolving its original references on a descendant.
+ sealed class CssInvalidValue : ICssValue
+ {
+ public static readonly CssInvalidValue Instance = new();
+
+ private CssInvalidValue()
+ {
+ }
+
+ public String CssText => String.Empty;
+
+ public ICssValue Compute(ICssComputeContext context) => this;
+
+ public Boolean Equals(ICssValue? other) => other is CssInvalidValue;
+ }
+}
diff --git a/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs b/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
index f1ab66ea..128bd9b3 100644
--- a/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
+++ b/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
@@ -17,6 +17,7 @@ public sealed class CssReferenceValue : ICssRawValue
private readonly String _value;
private readonly TextRange[] _ranges;
private readonly CssVarValue[] _references;
+ private readonly CssVariableValue _tokens;
#endregion
@@ -32,6 +33,16 @@ public CssReferenceValue(String value, IEnumerable
_value = value;
_ranges = references.Select(m => m.Item1).ToArray();
_references = references.Select(m => m.Item2).ToArray();
+ _tokens = new CssVariableValue(value);
+ }
+
+ internal CssReferenceValue(CssVariableValue value)
+ {
+ var references = value.GetReferences().ToArray();
+ _value = value.Text;
+ _ranges = references.Select(m => m.Item1).ToArray();
+ _references = references.Select(m => m.Item2).ToArray();
+ _tokens = value;
}
#endregion
@@ -64,17 +75,8 @@ public CssReferenceValue(String value, IEnumerable
ICssValue ICssValue.Compute(ICssComputeContext context)
{
- foreach (var reference in _references)
- {
- var result = reference.Compute(context);
-
- if (result is not null)
- {
- return result;
- }
- }
-
- return null;
+ var text = _tokens.Substitute(context.Resolve);
+ return text is null ? null : ((ICssValue)new CssAnyValue(text)).Compute(context);
}
Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other);
From 99c535a82bc15fdf9a354b156ae867f5c155ffc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Ros?=
<1165805+sebastienros@users.noreply.github.com>
Date: Sat, 5 Sep 2026 08:27:25 -0700
Subject: [PATCH 2/2] Preserve public declaration and variable value behavior
Keep variable resolution inside computed-style preparation; leave raw cascades and render-tree specified styles unchanged. Restore nested CssVarValue fallback trees and direct References-based computation, retaining iterative parsing, serialization, and fallback traversal.
Honor caller-modified references in custom-property and shorthand resolution. Add compatibility regressions for raw declarations, public parser cursor and value-tree behavior, mutable references, and deep public fallbacks.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
docs/general/02-Values.md | 13 +-
.../Styling/CustomPropertyCompatibility.cs | 244 ++++++++++++++++++
.../Dom/Internal/CssProperty.cs | 3 +-
.../Extensions/CssOmExtensions.cs | 2 +-
.../Extensions/StyleCollectionExtensions.cs | 35 ++-
.../Parser/Micro/FunctionParser.cs | 93 ++++++-
.../RenderTree/RenderTreeBuilder.cs | 12 +-
src/AngleSharp.Css/Values/CssChildValue.cs | 14 +-
.../Values/CssCustomPropertyResolver.cs | 43 ++-
src/AngleSharp.Css/Values/CssVariableValue.cs | 23 +-
.../Values/Functions/CssVarValue.cs | 56 ++--
src/AngleSharp.Css/Values/Raws/CssAnyValue.cs | 2 +-
.../Values/Raws/CssReferenceValue.cs | 69 ++++-
13 files changed, 525 insertions(+), 84 deletions(-)
create mode 100644 src/AngleSharp.Css.Tests/Styling/CustomPropertyCompatibility.cs
diff --git a/docs/general/02-Values.md b/docs/general/02-Values.md
index d4fa19db..9be72233 100644
--- a/docs/general/02-Values.md
+++ b/docs/general/02-Values.md
@@ -65,7 +65,12 @@ Console.WriteLine($"Computed font-size: {computedFontSize}");
## Custom Properties At Computed-Value Time
-Custom properties are resolved for each element before they are inherited. An inherited
+Custom properties are resolved only during style computation, for each element before
+they are inherited. `GetDeclarations`, `ComputeExplicitStyle`, `ComputeCascadedStyle`,
+and render-tree `SpecifiedStyle` retain the original variable expressions. Computed
+results are separate declarations and do not rewrite stylesheet or inline values.
+
+During computation, an inherited
alias keeps the parent's resolved value; changing its dependencies on a child does not
resolve that alias again. A declaration explicitly matching both elements is resolved
locally on each element.
@@ -78,7 +83,11 @@ or initial value, not an earlier declaration from the cascade. A valid custom-pr
value that does not match the consumer's grammar does not trigger the `var()` fallback.
Dependency analysis and fallback substitution are iterative, including deeply nested
-fallbacks. Expanded values are limited to 1,048,576 UTF-16 code units (including token
+fallbacks. The public parser still represents nested `var()` fallbacks as `CssVarValue`
+objects, and direct `CssReferenceValue.Compute` calls honor the supplied `References`
+array, including subsequent changes to its entries.
+
+Expanded values during style computation are limited to 1,048,576 UTF-16 code units (including token
separators) to bound exponential substitution; an expansion exceeding this limit is
invalid at computed-value time. Property-specific parsing, unit conversion, and layout
support still determine which resolved values can be used by a consuming property.
diff --git a/src/AngleSharp.Css.Tests/Styling/CustomPropertyCompatibility.cs b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCompatibility.cs
new file mode 100644
index 00000000..d5f750e2
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCompatibility.cs
@@ -0,0 +1,244 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Css.Parser;
+ using AngleSharp.Css.RenderTree;
+ using AngleSharp.Css.Values;
+ using AngleSharp.Dom;
+ using AngleSharp.Text;
+ using NUnit.Framework;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using static CssConstructionFunctions;
+
+ [TestFixture]
+ public class CustomPropertyCompatibilityTests
+ {
+ [TestCase("visible", "visible")]
+ [TestCase("var(--b)", "hidden")]
+ public void OnlyComputedStylesResolveCustomProperties(String value, String expected)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ var child = document.QuerySelector("span");
+ var styles = document.DefaultView.GetStyleCollection(new DefaultRenderDevice());
+ var explicitStyle = styles.ComputeExplicitStyle(element);
+ var declarations = styles.GetDeclarations(element);
+ var cascade = styles.ComputeCascadedStyle(child, declarations);
+ var builder = RenderTreeBuilder.GetInstance(document.DefaultView);
+ var rendered = builder.RenderElement(element, styles.Device);
+ var renderedChild = rendered.Children.OfType().Single();
+
+ foreach (var raw in new[] { explicitStyle, declarations, cascade, styles.GetDeclarations(child),
+ rendered.SpecifiedStyle, renderedChild.SpecifiedStyle, builder.GetElementStyle(child) })
+ {
+ Assert.AreEqual("var(--a)", raw.GetPropertyValue("--b"));
+ Assert.AreEqual("var(--b,hidden)", raw.GetPropertyValue("visibility"));
+ }
+
+ Assert.AreEqual(expected, styles.ComputeDeclarations(child).GetPropertyValue("visibility"));
+ Assert.AreEqual(expected, renderedChild.ComputedStyle.GetPropertyValue("visibility"));
+ Assert.AreEqual("var(--a)", declarations.GetPropertyValue("--b"));
+ }
+
+ [Test]
+ public void PublicFallbackParserPreservesNestedVariableObjects()
+ {
+ var source = new StringSource("var(--a,var(--b,red)))");
+ var outer = source.ParseVarFallback() as CssVarValue;
+ Assert.IsNotNull(outer);
+ Assert.AreEqual("--a", outer.VariableName);
+ var inner = outer.DefaultValue as CssVarValue;
+ Assert.IsNotNull(inner);
+ Assert.AreEqual("--b", inner.VariableName);
+ Assert.AreEqual("red", inner.DefaultValue.CssText);
+ Assert.AreEqual("var(--a, var(--b, red))", outer.CssText);
+ Assert.AreEqual(')', source.Current);
+ }
+
+ [Test]
+ public void ParsedReferencesPreserveNestedFallbackObjects()
+ {
+ var property = ParseDeclaration("visibility:var(--a,var(--b,var(--c,hidden)))");
+ var reference = (CssReferenceValue)property.RawValue;
+ var second = reference.References[0].DefaultValue as CssVarValue;
+ Assert.IsNotNull(second);
+ Assert.AreEqual("--b", second.VariableName);
+ var third = second.DefaultValue as CssVarValue;
+ Assert.IsNotNull(third);
+ Assert.AreEqual("--c", third.VariableName);
+ Assert.AreEqual("hidden", third.DefaultValue.CssText);
+ }
+
+ [Test]
+ public void PublicReferenceParserPreservesTheSourcePosition()
+ {
+ var source = new StringSource("var(--before) var(--after)");
+ source.NextTo("var(--before) ".Length);
+ var index = source.Index;
+ var reference = source.ParseVars();
+ Assert.AreEqual(index, source.Index);
+ Assert.AreEqual(1, reference.References.Length);
+ Assert.AreEqual("--after", reference.References[0].VariableName);
+ Assert.AreEqual("var(--before) var(--after)", reference.CssText);
+ Assert.AreEqual("after", ((ICssValue)reference).Compute(new TestComputeContext()).CssText);
+ }
+
+ [TestCase("var(--a)", false)]
+ [TestCase("var(--a,)", true)]
+ [TestCase("var(--a, )", true)]
+ public void EmptyFallbacksAreDistinctFromAbsentFallbacks(String text, Boolean hasFallback)
+ {
+ var source = new StringSource(text);
+ var reference = (CssVarValue)source.ParseVarFallback();
+ Assert.AreEqual("--a", reference.VariableName);
+ Assert.AreEqual(hasFallback, reference.DefaultValue is not null);
+ Assert.AreEqual(String.Empty, reference.DefaultValue?.CssText ?? String.Empty);
+ Assert.IsTrue(source.IsDone);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void DeepPublicFallbackTreesRemainIterative(Boolean parseDirectly)
+ {
+ const Int32 count = 8192;
+ var text = String.Concat(Enumerable.Repeat("var(--missing,", count)) + "visible" + new String(')', count);
+ var reference = parseDirectly ? (CssVarValue)new StringSource(text).ParseVarFallback() :
+ ((CssReferenceValue)ParseDeclaration("visibility:" + text).RawValue).References[0];
+ var current = reference;
+ var depth = 1;
+
+ while (current.DefaultValue is CssVarValue nested)
+ {
+ current = nested;
+ depth++;
+ }
+
+ Assert.AreEqual(count, depth);
+ Assert.AreEqual("visible", current.DefaultValue.CssText);
+ Assert.AreEqual(text.Replace(",", ", "), reference.CssText);
+ var context = new TestComputeContext { Converter = ParseDeclaration("visibility:visible").Converter };
+ Assert.AreEqual("visible", reference.Compute(context).CssText);
+ }
+
+ [Test]
+ public void DirectReferenceComputationUsesSuppliedAndMutableReferences()
+ {
+ var reference = new CssReferenceValue("var(--literal)", new[]
+ {
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--supplied")),
+ });
+ var context = new TestComputeContext();
+ Assert.AreEqual("supplied", ((ICssValue)reference).Compute(context).CssText);
+ reference.References[0] = new CssVarValue("--modified");
+ Assert.AreEqual("modified", ((ICssValue)reference).Compute(context).CssText);
+ Assert.AreEqual("var(--literal)", reference.CssText);
+ }
+
+ [Test]
+ public void DirectReferenceComputationRetainsFirstSuccessfulReference()
+ {
+ var reference = new CssReferenceValue("var(--literal)", new[]
+ {
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--missing")),
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--supplied")),
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--unused")),
+ });
+ var context = new TestComputeContext();
+ Assert.AreEqual("supplied", ((ICssValue)reference).Compute(context).CssText);
+ CollectionAssert.AreEqual(new[] { "--missing", "--supplied" }, context.Names);
+ }
+
+ [Test]
+ public void ModifiedParsedReferencesAffectComputedStyles()
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ var reference = (CssReferenceValue)element.GetStyle().GetProperty("visibility").RawValue;
+ reference.References[0] = new CssVarValue("--b");
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual("var(--a)", reference.CssText);
+ }
+
+ [TestCase("--b", "hidden")]
+ [TestCase("--alias", "collapse")]
+ public void ModifiedCustomPropertyReferencesParticipateInResolution(String name, String expected)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ var reference = (CssReferenceValue)element.GetStyle().GetProperty("--alias").RawValue;
+ reference.References[0] = new CssVarValue(name);
+ Assert.AreEqual(expected, element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ Assert.AreEqual("var(--a)", element.GetStyle().GetPropertyValue("--alias"));
+ }
+
+ [Test]
+ public void ConstructedCustomPropertyReferencesAreNotReparsedFromLiteralText()
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ ((CssProperty)element.GetStyle().GetProperty("--alias")).RawValue = new CssReferenceValue("var(--a)", new[]
+ {
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--missing")),
+ Tuple.Create(new TextRange(default, default), new CssVarValue("--b")),
+ });
+ Assert.AreEqual("hidden", element.ComputeCurrentStyle().GetPropertyValue("visibility"));
+ }
+
+ [Test]
+ public void ModifiedShorthandReferencesParticipateInResolution()
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("div");
+ var child = (CssChildValue)element.GetStyle().GetProperty("margin-top").RawValue;
+ var reference = (CssReferenceValue)child.Parent;
+ reference.References[0] = new CssVarValue("--b");
+ var computed = element.ComputeCurrentStyle();
+ Assert.AreEqual("3px", computed.GetPropertyValue("margin-top"));
+ Assert.AreEqual("4px", computed.GetPropertyValue("margin-right"));
+ }
+
+ [Test]
+ public void DirectVariableComputationRetainsFallbackOnFailedComputation()
+ {
+ var reference = new CssVarValue("--invalid", new CssIdentifierValue("fallback"));
+ var context = new TestComputeContext();
+ Assert.AreEqual("fallback", reference.Compute(context).CssText);
+ }
+
+ [Test]
+ public void ComputationDoesNotSuppressValueExceptions()
+ {
+ var reference = new CssVarValue("--throw", new CssIdentifierValue("fallback"));
+ Assert.Throws(() => reference.Compute(new TestComputeContext()));
+ }
+
+ private sealed class TestComputeContext : ICssComputeContext
+ {
+ public IRenderDevice Device { get; } = new DefaultRenderDevice();
+ public IBrowsingContext Context => null;
+ public IValueConverter Converter { get; set; }
+ public List Names { get; } = new();
+
+ public ICssValue Resolve(String name)
+ {
+ Names.Add(name);
+
+ if (name == "--invalid")
+ {
+ return new CssAnyValue("not a value");
+ }
+
+ if (name == "--throw")
+ {
+ throw new InvalidOperationException("Test exception");
+ }
+
+ return name == "--missing" ? null : new CssIdentifierValue(name.Substring(2));
+ }
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
index c9128d86..3a1413a4 100644
--- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
+++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs
@@ -113,7 +113,8 @@ public ICssProperty Compute(ICssComputeContext context)
var propertyContext = new PropertyComputeContext(context, _converter);
var computedValue = _name.StartsWith("--", StringComparison.Ordinal) ?
context.Resolve(_name) ?? CssInvalidValue.Instance :
- _value is CssChildValue child ? child.Compute(propertyContext, _name) : _value?.Compute(propertyContext);
+ _value is CssChildValue child ? child.Compute(propertyContext, _name) :
+ _value is CssReferenceValue reference ? reference.ComputeSubstituted(propertyContext) : _value?.Compute(propertyContext);
if (computedValue != _value)
{
diff --git a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
index e4882f60..c57b2564 100644
--- a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
@@ -114,7 +114,7 @@ public static ICssStyleDeclaration Compute(this ICssStyleDeclaration style, ICss
return computedStyle;
}
- internal static CssStyleDeclaration Cascade(this ICssStyleDeclaration style, ICssStyleDeclaration parent, ICssComputeContext context)
+ internal static CssStyleDeclaration PrepareComputedDeclarations(this ICssStyleDeclaration style, ICssStyleDeclaration parent, ICssComputeContext context)
{
var declarations = new CssStyleDeclaration(context.Context);
diff --git a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
index 408ff3d5..50606462 100644
--- a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
@@ -44,7 +44,7 @@ public static IStyleCollection GetStyleCollection(this IWindow window, IRenderDe
/// The optional pseudo selector to use.
/// The style declaration containing all the declarations.
public static ICssStyleDeclaration ComputeDeclarations(this IStyleCollection styles, IElement element, String? pseudoSelector = null)
- => GetElementDeclarations(styles, element, pseudoSelector, true);
+ => GetComputedDeclarations(styles, element, pseudoSelector);
///
/// Gets the declarations for the given element in the context of
@@ -55,9 +55,27 @@ public static ICssStyleDeclaration ComputeDeclarations(this IStyleCollection sty
/// The optional pseudo selector to use.
/// The style declaration containing all the declarations.
public static ICssStyleDeclaration GetDeclarations(this IStyleCollection styles, IElement element, String? pseudoSelector = null)
- => GetElementDeclarations(styles, element, pseudoSelector, false);
+ {
+ var ctx = element.Owner?.Context;
+ var declarations = new CssStyleDeclaration(ctx);
+ var ancestors = element.GetAncestors().OfType();
+
+ if (!String.IsNullOrEmpty(pseudoSelector))
+ {
+ element = element.Pseudo(pseudoSelector!.TrimStart(':')) ?? element;
+ }
+
+ declarations.SetDeclarations(styles.ComputeExplicitStyle(element));
- private static ICssStyleDeclaration GetElementDeclarations(IStyleCollection styles, IElement element, String? pseudoSelector, Boolean compute)
+ foreach (var ancestor in ancestors)
+ {
+ declarations.UpdateDeclarations(styles.ComputeExplicitStyle(ancestor));
+ }
+
+ return declarations;
+ }
+
+ private static ICssStyleDeclaration GetComputedDeclarations(IStyleCollection styles, IElement element, String? pseudoSelector)
{
var ctx = element.Owner?.Context;
ICssStyleDeclaration? parent = null;
@@ -84,8 +102,7 @@ private static ICssStyleDeclaration GetElementDeclarations(IStyleCollection styl
{
var explicitStyle = styles.ComputeExplicitStyle(nodes.Pop());
var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parent);
- var declarations = explicitStyle.Cascade(parent!, context);
- parent = compute ? declarations.Compute(context) : declarations;
+ parent = explicitStyle.PrepareComputedDeclarations(parent!, context).Compute(context);
}
return parent!;
@@ -101,9 +118,9 @@ private static ICssStyleDeclaration GetElementDeclarations(IStyleCollection styl
/// Returns the cascaded read-only style declaration.
public static ICssStyleDeclaration ComputeCascadedStyle(this IStyleCollection styles, IElement element, ICssStyleDeclaration parent)
{
- var explicitStyle = styles.ComputeExplicitStyle(element);
- var context = new CssComputeContext(styles.Device, element.Owner?.Context, explicitStyle, parent);
- return explicitStyle.Cascade(parent, context);
+ var declarations = (CssStyleDeclaration)styles.ComputeExplicitStyle(element);
+ declarations.UpdateDeclarations(parent);
+ return declarations;
}
///
@@ -147,7 +164,7 @@ internal static ICssStyleDeclaration ComputeDeclarationsWithParent(this IStyleCo
var ctx = element.Owner?.Context;
var explicitStyle = styles.ComputeExplicitStyle(element);
var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parentComputedStyle);
- return explicitStyle.Cascade(parentComputedStyle, context).Compute(context);
+ return explicitStyle.PrepareComputedDeclarations(parentComputedStyle, context).Compute(context);
}
#endregion
diff --git a/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs b/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
index ffd9cd63..dbc20746 100644
--- a/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
+++ b/src/AngleSharp.Css/Parser/Micro/FunctionParser.cs
@@ -5,6 +5,7 @@ namespace AngleSharp.Css.Parser
using AngleSharp.Css.Values;
using AngleSharp.Text;
using System;
+ using System.Collections.Generic;
///
/// Represents extensions to for general CSS functions.
@@ -36,14 +37,54 @@ public static CssAttrValue ParseAttr(this StringSource source)
///
public static CssReferenceValue ParseVars(this StringSource source)
{
- if (source.Content.IndexOf(FunctionNames.Var, StringComparison.OrdinalIgnoreCase) < 0 &&
- source.Content.IndexOf('\\') < 0)
+ var index = source.Index;
+ var start = index;
+ var length = FunctionNames.Var.Length;
+ var refs = default(List>);
+
+ while (!source.IsDone)
{
- return null;
+ index = source.Content.IndexOf(FunctionNames.Var, index, StringComparison.OrdinalIgnoreCase) + length;
+
+ if (index >= length)
+ {
+ source.NextTo(index);
+ var c = source.SkipSpacesAndComments();
+
+ if (c == Symbols.RoundBracketOpen)
+ {
+ source.SkipCurrentAndSpaces();
+ var s = new TextPosition(0, 0, source.Index);
+ var reference = ParseVar(source);
+
+ if (reference == null)
+ {
+ refs = null;
+ break;
+ }
+
+ if (refs == null)
+ {
+ refs = new List>();
+ }
+
+ var e = new TextPosition(0, 0, source.Index);
+ refs.Add(Tuple.Create(new TextRange(s, e), reference));
+ continue;
+ }
+ }
+
+ break;
}
- var value = new CssVariableValue(source.Content);
- return value.IsValid && value.HasReferences ? new CssReferenceValue(value) : null;
+ source.BackTo(start);
+
+ if (refs != null)
+ {
+ return new CssReferenceValue(new CssVariableValue(source.Content), refs);
+ }
+
+ return null;
}
///
@@ -75,9 +116,45 @@ public static CssVarValue ParseVar(this StringSource source)
///
public static ICssValue ParseVarFallback(this StringSource source)
{
- var content = source.TakeUntilClosed();
- source.SkipCurrentAndSpaces();
- return new CssAnyValue(content);
+ var names = new Stack();
+ ICssValue fallback = null;
+ var readFallback = true;
+
+ while (source.IsFunction(FunctionNames.Var))
+ {
+ var name = source.ParseCustomIdent();
+ var separator = source.SkipGetSkip();
+
+ if (name is null || (separator != Symbols.Comma && separator != Symbols.RoundBracketClose))
+ {
+ readFallback = false;
+ break;
+ }
+
+ names.Push(name);
+
+ if (separator == Symbols.RoundBracketClose)
+ {
+ readFallback = false;
+ break;
+ }
+
+ source.SkipSpacesAndComments();
+ }
+
+ if (readFallback)
+ {
+ var content = source.TakeUntilClosed();
+ source.SkipCurrentAndSpaces();
+ fallback = new CssAnyValue(content);
+ }
+
+ while (names.Count > 0)
+ {
+ fallback = new CssVarValue(names.Pop(), fallback);
+ }
+
+ return fallback;
}
///
diff --git a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
index 75e93e3a..bdbc9070 100644
--- a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
+++ b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
@@ -84,10 +84,16 @@ private ElementRenderNode RenderElement(
{
var explicitStyle = collection.ComputeExplicitStyle(element);
- var specifiedContext = new CssComputeContext(collection.Device, _context, explicitStyle, parentSpecifiedStyle);
- var specifiedStyle = explicitStyle.Cascade(parentSpecifiedStyle!, specifiedContext);
+ var specifiedStyle = new CssStyleDeclaration(_context);
+ specifiedStyle.SetDeclarations(explicitStyle);
+
+ if (parentSpecifiedStyle is not null)
+ {
+ specifiedStyle.UpdateDeclarations(parentSpecifiedStyle);
+ }
+
var computeContext = new CssComputeContext(collection.Device, _context, explicitStyle, parentComputedStyle);
- var computedStyle = explicitStyle.Cascade(parentComputedStyle!, computeContext).Compute(computeContext);
+ var computedStyle = explicitStyle.PrepareComputedDeclarations(parentComputedStyle!, computeContext).Compute(computeContext);
var children = new List();
var node = new ElementRenderNode(element, parent, children, specifiedStyle, computedStyle);
diff --git a/src/AngleSharp.Css/Values/CssChildValue.cs b/src/AngleSharp.Css/Values/CssChildValue.cs
index 3919fca8..6ba38a1b 100644
--- a/src/AngleSharp.Css/Values/CssChildValue.cs
+++ b/src/AngleSharp.Css/Values/CssChildValue.cs
@@ -93,7 +93,19 @@ internal ICssValue Compute(ICssComputeContext context, String longhandName)
if (shorthandName is not null && parent is ICssRawValue)
{
- var text = new CssVariableValue(parent.CssText).Substitute(context.Resolve);
+ var values = parent is CssReferenceValue reference ? reference.GetVariableValues() :
+ new[] { new CssVariableValue(parent.CssText) };
+ String text = null;
+
+ foreach (var candidate in values)
+ {
+ text = candidate.Substitute(context.Resolve);
+
+ if (text is not null)
+ {
+ break;
+ }
+ }
if (text is null)
{
diff --git a/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs b/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
index 56ff1102..926b4eb3 100644
--- a/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
+++ b/src/AngleSharp.Css/Values/CssCustomPropertyResolver.cs
@@ -3,6 +3,7 @@ namespace AngleSharp.Css.Values
using AngleSharp.Css.Dom;
using System;
using System.Collections.Generic;
+ using System.Linq;
sealed class CssCustomPropertyResolver
{
@@ -35,8 +36,10 @@ public CssCustomPropertyResolver(IEnumerable properties, ICssPrope
continue;
}
- var tokens = value is null || value is CssInvalidValue ? null : new CssVariableValue(value.CssText);
- var keyword = tokens?.Keyword;
+ var values = value is CssReferenceValue references ? references.GetVariableValues().ToArray() :
+ value is null || value is CssInvalidValue ? Array.Empty() :
+ new[] { new CssVariableValue(value.CssText) };
+ var keyword = values.Length == 1 ? values[0].Keyword : null;
if (String.Equals(keyword, CssKeywords.Inherit, StringComparison.OrdinalIgnoreCase) ||
String.Equals(keyword, CssKeywords.Unset, StringComparison.OrdinalIgnoreCase))
@@ -46,20 +49,28 @@ public CssCustomPropertyResolver(IEnumerable properties, ICssPrope
_values[property.Name] = null;
- if (tokens is not null && tokens.IsValid && !String.Equals(keyword, CssKeywords.Initial, StringComparison.OrdinalIgnoreCase))
+ if (values.Length > 0 && !String.Equals(keyword, CssKeywords.Initial, StringComparison.OrdinalIgnoreCase))
{
- nodes[property.Name] = new Node(property.Name, tokens);
+ nodes[property.Name] = new Node(property.Name, values);
}
}
}
foreach (var node in nodes.Values)
{
- foreach (var name in node.Value.Dependencies)
+ foreach (var value in node.Values)
{
- if (nodes.TryGetValue(name, out var dependency))
+ if (!value.IsValid)
{
- node.Dependencies.Add(dependency);
+ continue;
+ }
+
+ foreach (var name in value.Dependencies)
+ {
+ if (nodes.TryGetValue(name, out var dependency))
+ {
+ node.Dependencies.Add(dependency);
+ }
}
}
}
@@ -123,8 +134,16 @@ public CssCustomPropertyResolver(IEnumerable properties, ICssPrope
if (component.Count == 1 && !node.Dependencies.Contains(node))
{
- var text = node.Value.Substitute(Resolve);
- _values[node.Name] = text is null ? null : new CssAnyValue(text, isResolved: true);
+ foreach (var value in node.Values)
+ {
+ var text = value.Substitute(Resolve);
+
+ if (text is not null)
+ {
+ _values[node.Name] = new CssAnyValue(text, isResolved: true);
+ break;
+ }
+ }
}
}
}
@@ -143,14 +162,14 @@ void Enter(Node node)
private sealed class Node
{
- public Node(String name, CssVariableValue value)
+ public Node(String name, CssVariableValue[] values)
{
Name = name;
- Value = value;
+ Values = values;
}
public String Name { get; }
- public CssVariableValue Value { get; }
+ public CssVariableValue[] Values { get; }
public List Dependencies { get; } = new();
public Int32 Index { get; set; } = -1;
public Int32 LowLink { get; set; }
diff --git a/src/AngleSharp.Css/Values/CssVariableValue.cs b/src/AngleSharp.Css/Values/CssVariableValue.cs
index 3cd2eba0..85d403b7 100644
--- a/src/AngleSharp.Css/Values/CssVariableValue.cs
+++ b/src/AngleSharp.Css/Values/CssVariableValue.cs
@@ -79,7 +79,7 @@ public CssVariableValue(String text)
if (valid)
{
- _references.Add(i, new Reference(_tokens[name].Data, name, end, separator < end ? separator + 1 : -1));
+ _references.Add(i, new Reference(_tokens[name].Data, end, separator < end ? separator + 1 : -1));
}
}
}
@@ -112,23 +112,6 @@ public String? Keyword
}
}
- public IEnumerable> GetReferences()
- {
- for (var i = 0; i < _tokens.Count; i++)
- {
- if (_references.TryGetValue(i, out var reference))
- {
- var fallback = reference.Fallback >= 0 ?
- new CssAnyValue(Text.Substring(Offset(reference.Fallback - 1) + 1,
- Offset(reference.End) - Offset(reference.Fallback - 1) - 1).Trim()) : null;
- var start = new TextPosition(0, 0, Offset(reference.NameIndex));
- var end = new TextPosition(0, 0, EndOffset(reference.End));
- yield return Tuple.Create(new TextRange(start, end), new CssVarValue(reference.Name, fallback));
- i = reference.End;
- }
- }
- }
-
public String? Substitute(Func resolve)
{
if (!IsValid)
@@ -262,16 +245,14 @@ private static CssTokenType CloseType(CssTokenType type) =>
private readonly struct Reference
{
- public Reference(String name, Int32 nameIndex, Int32 end, Int32 fallback)
+ public Reference(String name, Int32 end, Int32 fallback)
{
Name = name;
- NameIndex = nameIndex;
End = end;
Fallback = fallback;
}
public String Name { get; }
- public Int32 NameIndex { get; }
public Int32 End { get; }
public Int32 Fallback { get; }
}
diff --git a/src/AngleSharp.Css/Values/Functions/CssVarValue.cs b/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
index d9d4167d..29a70689 100644
--- a/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
+++ b/src/AngleSharp.Css/Values/Functions/CssVarValue.cs
@@ -5,6 +5,7 @@ namespace AngleSharp.Css.Values
using AngleSharp.Text;
using System;
using System.Collections.Generic;
+ using System.Text;
///
/// Represents a CSS var replacement.
@@ -78,18 +79,30 @@ public String CssText
{
get
{
- var fn = FunctionNames.Var;
- var args = new List
- {
- _variableName,
- };
+ var text = new StringBuilder();
+ var value = this;
+ var depth = 0;
- if (_defaultValue is not null)
+ while (true)
{
- args.Add(_defaultValue.CssText);
- }
+ text.Append(FunctionNames.Var).Append('(').Append(value._variableName);
+ depth++;
+
+ if (value._defaultValue is not null)
+ {
+ text.Append(", ");
- return fn.CssFunction(String.Join(", ", args));
+ if (value._defaultValue is CssVarValue nested)
+ {
+ value = nested;
+ continue;
+ }
+
+ text.Append(value._defaultValue.CssText);
+ }
+
+ return text.Append(')', depth).ToString();
+ }
}
}
@@ -121,20 +134,25 @@ public Boolean Equals(CssVarValue other)
/// The resolved value or null.
public ICssValue Compute(ICssComputeContext context)
{
- var value = context.Resolve(_variableName);
+ var reference = this;
- if (value is not null)
+ while (true)
{
- return value.Compute(context);
- }
+ var value = context.Resolve(reference._variableName)?.Compute(context);
- if (_defaultValue is null)
- {
- return null;
- }
+ if (value is not null)
+ {
+ return value;
+ }
- var text = new CssVariableValue(_defaultValue.CssText).Substitute(context.Resolve);
- return text is null ? null : ((ICssValue)new CssAnyValue(text)).Compute(context);
+ if (reference._defaultValue is CssVarValue nested)
+ {
+ reference = nested;
+ continue;
+ }
+
+ return reference._defaultValue?.Compute(context);
+ }
}
Boolean IEquatable.Equals(ICssValue other) => other is CssVarValue value && Equals(value);
diff --git a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
index 5b3fa40c..33418082 100644
--- a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
+++ b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
@@ -64,7 +64,7 @@ ICssValue ICssValue.Compute(ICssComputeContext context)
return source.IsDone ? value?.Compute(context) : null;
}
- return this;
+ return IsResolved ? this : null;
}
Boolean IEquatable.Equals(ICssValue other) => other is CssAnyValue o && _text == o.CssText;
diff --git a/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs b/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
index 128bd9b3..b1844e46 100644
--- a/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
+++ b/src/AngleSharp.Css/Values/Raws/CssReferenceValue.cs
@@ -18,6 +18,7 @@ public sealed class CssReferenceValue : ICssRawValue
private readonly TextRange[] _ranges;
private readonly CssVarValue[] _references;
private readonly CssVariableValue _tokens;
+ private readonly CssVarValue[] _parsedReferences;
#endregion
@@ -33,15 +34,12 @@ public CssReferenceValue(String value, IEnumerable
_value = value;
_ranges = references.Select(m => m.Item1).ToArray();
_references = references.Select(m => m.Item2).ToArray();
- _tokens = new CssVariableValue(value);
}
- internal CssReferenceValue(CssVariableValue value)
+ internal CssReferenceValue(CssVariableValue value, IEnumerable> references)
+ : this(value.Text, references)
{
- var references = value.GetReferences().ToArray();
- _value = value.Text;
- _ranges = references.Select(m => m.Item1).ToArray();
- _references = references.Select(m => m.Item2).ToArray();
+ _parsedReferences = (CssVarValue[])_references.Clone();
_tokens = value;
}
@@ -75,10 +73,69 @@ internal CssReferenceValue(CssVariableValue value)
ICssValue ICssValue.Compute(ICssComputeContext context)
{
+ foreach (var reference in _references)
+ {
+ var result = reference.Compute(context);
+
+ if (result is not null)
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ internal ICssValue ComputeSubstituted(ICssComputeContext context)
+ {
+ // Direct value computation retains the public References contract.
+ // Only unmodified parser-owned values use token-stream substitution
+ // at the property computation boundary.
+ if (HasCustomReferences)
+ {
+ return ((ICssValue)this).Compute(context);
+ }
+
var text = _tokens.Substitute(context.Resolve);
return text is null ? null : ((ICssValue)new CssAnyValue(text)).Compute(context);
}
+ internal IEnumerable GetVariableValues()
+ {
+ if (HasCustomReferences)
+ {
+ foreach (var reference in _references)
+ {
+ yield return new CssVariableValue(reference.CssText);
+ }
+ }
+ else
+ {
+ yield return _tokens;
+ }
+ }
+
+ private Boolean HasCustomReferences
+ {
+ get
+ {
+ if (_tokens is null)
+ {
+ return true;
+ }
+
+ for (var i = 0; i < _references.Length; i++)
+ {
+ if (!Object.ReferenceEquals(_references[i], _parsedReferences[i]))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other);
#endregion