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;