diff --git a/src/AngleSharp.Css.Tests/Styling/IndividualTransformComputation.cs b/src/AngleSharp.Css.Tests/Styling/IndividualTransformComputation.cs
new file mode 100644
index 0000000..0c4a631
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/IndividualTransformComputation.cs
@@ -0,0 +1,124 @@
+#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.Threading.Tasks;
+ using static CssConstructionFunctions;
+
+ [TestFixture]
+ public class IndividualTransformComputationTests
+ {
+ [Test]
+ public async Task OriginalReproductionDoesNotOverflow()
+ {
+ using var context = BrowsingContext.New(Configuration.Default.WithCss());
+ using var document = await context.OpenAsync(request => request.Content(
+ "
"));
+
+ Assert.AreEqual("1px", document.QuerySelector("div").ComputeCurrentStyle().GetPropertyValue("translate"));
+ }
+
+ [TestCase("translate", "1px")]
+ [TestCase("translate", "1px 2px")]
+ [TestCase("translate", "1px 2px 3px")]
+ [TestCase("translate", "50% 25%")]
+ [TestCase("translate", "0")]
+ [TestCase("translate", "0 -50%")]
+ [TestCase("translate", "none")]
+ [TestCase("rotate", "1deg")]
+ [TestCase("rotate", "45deg")]
+ [TestCase("rotate", "x 45deg")]
+ [TestCase("rotate", "1 0 0 45deg")]
+ [TestCase("rotate", "none")]
+ [TestCase("scale", "1")]
+ [TestCase("scale", "1.5 2")]
+ [TestCase("scale", "1 1.5 2")]
+ [TestCase("scale", "none")]
+ public void IndividualTransformsComputeWithoutChangingSpecifiedStyles(String name, String value)
+ {
+ using var document = ParseDocument("");
+ var element = document.QuerySelector("div");
+ var styles = document.DefaultView.GetStyleCollection(new DefaultRenderDevice());
+ var specified = styles.GetDeclarations(element);
+ var source = specified.CssText;
+ var rendered = RenderTreeBuilder.GetInstance(document.DefaultView).RenderElement(element, styles.Device);
+
+ Assert.AreEqual(value, element.ComputeCurrentStyle().GetPropertyValue(name));
+ Assert.AreEqual(value, rendered.ComputedStyle.GetPropertyValue(name));
+ Assert.AreEqual(value, rendered.SpecifiedStyle.GetPropertyValue(name));
+ Assert.AreEqual(source, specified.CssText);
+ Assert.AreEqual(value, element.ComputeCurrentStyle().GetPropertyValue(name));
+ }
+
+ [TestCase("translate", "1px", "2px", "1px 2px")]
+ [TestCase("rotate", "x", "45deg", "x 45deg")]
+ [TestCase("scale", "1", "2", "1 2")]
+ public void SubstitutionPreservesAllComponentsAndInheritedAliases(String name, String first, String second, String expected)
+ {
+ using var document = ParseDocument("" +
+ "
");
+ var element = document.QuerySelector("span");
+ var computed = element.ComputeCurrentStyle();
+
+ Assert.AreEqual(expected, computed.GetPropertyValue(name));
+ Assert.AreEqual(first, computed.GetPropertyValue("--alias"));
+ Assert.AreEqual("var(--alias) var(--b)", element.GetStyle().GetPropertyValue(name));
+ }
+
+ [TestCase("translate", "1px")]
+ [TestCase("rotate", "45deg")]
+ [TestCase("scale", "2")]
+ public void MissingAndCyclicVariablesStillUseFallbacksOrInitialValues(String name, String fallback)
+ {
+ using var document = ParseDocument("");
+ var element = document.QuerySelector("div");
+ var computed = element.ComputeCurrentStyle();
+
+ Assert.AreEqual(fallback, computed.GetPropertyValue(name));
+ Assert.AreEqual("rgba(255, 0, 0, 1)", computed.GetPropertyValue("color"));
+ Assert.IsInstanceOf(computed.GetProperty("--a").RawValue);
+ Assert.IsInstanceOf(computed.GetProperty("--b").RawValue);
+
+ foreach (var value in new[] { "var(--a)", "var(--missing)", "var(--missing,none trailing)" })
+ {
+ element.GetStyle().SetProperty(name, value);
+ Assert.AreEqual("none", element.ComputeCurrentStyle().GetPropertyValue(name), value);
+ }
+ }
+
+ [TestCase("translate", "1px")]
+ [TestCase("rotate", "45deg")]
+ [TestCase("scale", "2")]
+ public void InheritanceAndSubstitutedCssWideKeywordsRetainTheirBehavior(String name, String parentValue)
+ {
+ using var document = ParseDocument("
");
+ var element = document.QuerySelector("span");
+ element.SetAttribute("style", name + ":inherit");
+ Assert.AreEqual(parentValue, element.ComputeCurrentStyle().GetPropertyValue(name));
+
+ foreach (var keyword in new[] { "inherit", "initial", "unset" })
+ {
+ element.SetAttribute("style", "--keyword:var(--missing," + keyword + ");" + name + ":var(--keyword)");
+ Assert.AreEqual(keyword == "inherit" ? parentValue : "none",
+ element.ComputeCurrentStyle().GetPropertyValue(name), keyword);
+ }
+ }
+
+ [Test]
+ public void InvalidConcreteValuesStillDefaultInsteadOfExposingEarlierDeclarations()
+ {
+ using var document = ParseDocument("
");
+ var computed = document.QuerySelector("span").ComputeCurrentStyle();
+
+ Assert.AreEqual("auto", computed.GetPropertyValue("width"));
+ Assert.AreEqual("hidden", computed.GetPropertyValue("visibility"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Values/AnyValueComputation.cs b/src/AngleSharp.Css.Tests/Values/AnyValueComputation.cs
new file mode 100644
index 0000000..d41b6ee
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Values/AnyValueComputation.cs
@@ -0,0 +1,127 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Values
+{
+ using AngleSharp.Css.Converters;
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Css.Values;
+ using AngleSharp.Text;
+ using NUnit.Framework;
+ using System;
+ using static ValueConverters;
+
+ [TestFixture]
+ public class AnyValueComputationTests
+ {
+ [TestCase(false)]
+ [TestCase(true)]
+ public void AnyResultIsNotRecomputedThroughNestedConverters(Boolean isResolved)
+ {
+ var converter = new SingleUseConverter(Or(None, Or(Auto, Any)));
+ var context = new TestComputeContext { Converter = converter };
+ ICssValue value = new CssAnyValue("opaque tokens", isResolved);
+
+ Assert.AreEqual("opaque tokens", value.Compute(context).CssText);
+ Assert.AreEqual(1, converter.Calls);
+ }
+
+ [TestCase("none", "none")]
+ [TestCase("2em", "32px")]
+ [TestCase("calc(1px + 2px)", "3px")]
+ [TestCase(" /*before*/ 2em /*after*/ ", "32px")]
+ [TestCase("invalid", null)]
+ [TestCase("2px trailing", null)]
+ [TestCase("none trailing", null)]
+ public void ConcreteCompositeResultsStillComputeAndValidate(String text, String expected)
+ {
+ var context = new TestComputeContext { Converter = Or(None, LengthConverter) };
+
+ foreach (var isResolved in new[] { false, true })
+ {
+ ICssValue value = new CssAnyValue(text, isResolved);
+ Assert.AreEqual(expected, value.Compute(context)?.CssText);
+ }
+ }
+
+ [TestCase("none", "none")]
+ [TestCase("2em", "32px")]
+ [TestCase("calc(1px + 2px)", "3px")]
+ public void ConcreteBranchesBeforeAnyStillCompute(String text, String expected)
+ {
+ var context = new TestComputeContext { Converter = Or(None, LengthConverter, Any) };
+ ICssValue value = new CssAnyValue(text);
+
+ Assert.AreEqual(expected, value.Compute(context).CssText);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void AnyResultStillRequiresCompleteInputConsumption(Boolean isResolved)
+ {
+ var context = new TestComputeContext
+ {
+ Converter = new ClassValueConverter(source =>
+ {
+ source.Next();
+ return new CssAnyValue("accepted", isResolved);
+ }),
+ };
+ ICssValue value = new CssAnyValue("xy", isResolved);
+
+ Assert.IsNull(value.Compute(context));
+ }
+
+ [TestCase(false, false)]
+ [TestCase(false, true)]
+ [TestCase(true, false)]
+ [TestCase(true, true)]
+ public void DirectAnyAndMissingConvertersRetainResolutionSemantics(Boolean isResolved, Boolean hasConverter)
+ {
+ var context = new TestComputeContext { Converter = hasConverter ? Any : null };
+ ICssValue value = new CssAnyValue("opaque tokens", isResolved);
+
+ Assert.AreSame(isResolved ? value : null, value.Compute(context));
+ }
+
+ [Test]
+ public void ConverterExceptionsAreNotSuppressed()
+ {
+ var context = new TestComputeContext
+ {
+ Converter = new ClassValueConverter(_ => throw new InvalidOperationException("Test exception")),
+ };
+ ICssValue value = new CssAnyValue("opaque tokens");
+
+ Assert.Throws(() => value.Compute(context));
+ }
+
+ private sealed class SingleUseConverter : IValueConverter
+ {
+ private readonly IValueConverter _converter;
+
+ public SingleUseConverter(IValueConverter converter)
+ {
+ _converter = converter;
+ }
+
+ public Int32 Calls { get; private set; }
+
+ public ICssValue Convert(StringSource source)
+ {
+ if (++Calls > 1)
+ {
+ throw new InvalidOperationException("The converter was re-entered for its own any result.");
+ }
+
+ return _converter.Convert(source);
+ }
+ }
+
+ private sealed class TestComputeContext : ICssComputeContext
+ {
+ public IRenderDevice Device { get; } = new DefaultRenderDevice { FontSize = 16 };
+ public IBrowsingContext Context => null;
+ public IValueConverter Converter { get; set; }
+ public ICssValue Resolve(String name) => null;
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
index 3341808..e6fd1de 100644
--- a/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
+++ b/src/AngleSharp.Css/Values/Raws/CssAnyValue.cs
@@ -61,7 +61,15 @@ ICssValue ICssValue.Compute(ICssComputeContext context)
source.SkipSpacesAndComments();
var value = converter.Convert(source);
source.SkipSpacesAndComments();
- return source.IsDone ? value?.Compute(context) : null;
+
+ if (!source.IsDone)
+ {
+ return null;
+ }
+
+ // A composite converter may accept opaque tokens through an Any
+ // arm. Recomputing that result would re-enter the same converter.
+ return value is CssAnyValue ? value : value?.Compute(context);
}
return IsResolved ? this : null;