diff --git a/src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs b/src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs new file mode 100644 index 0000000..9846940 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs @@ -0,0 +1,81 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Declarations +{ + using AngleSharp.Css.Dom; + using NUnit.Framework; + using System; + using System.Collections.Generic; + using System.Linq; + using static CssConstructionFunctions; + + /// + /// Setting a declaration builds a fresh property and assigns its value, which always + /// replaces the raw value it starts with. These cases pin down that re-declaring a + /// property - longhand over shorthand, shorthand over longhand, important over + /// normal, or an invalid value over a valid one - does not depend on the new property + /// being seeded from the old one first. + /// + /// The expectations are a captured baseline of the behaviour, not a statement about + /// what the spec requires. + /// + [TestFixture] + public class SetPropertyOverwriteTests + { + private static readonly (String Input, String Expected)[] Cases = + { + ("color:red;color:blue", "color: rgba(0, 0, 255, 1)"), + ("color:blue;color:red", "color: rgba(255, 0, 0, 1)"), + ("color:red;color:notacolor", "color: rgba(255, 0, 0, 1)"), + ("color:red !important;color:blue", "color: rgba(255, 0, 0, 1) !important"), + ("color:red;color:blue !important", "color: rgba(0, 0, 255, 1) !important"), + ("color:red !important;color:blue !important", "color: rgba(0, 0, 255, 1) !important"), + ("--x:1;--x:2", "--x: 2"), + ("--x:1;color:red", "--x: 1; color: rgba(255, 0, 0, 1)"), + ("margin:1px;margin-top:2px", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 2px"), + ("margin-top:2px;margin:1px", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 1px"), + ("margin:1px;margin:notalength", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 1px"), + ("margin:1px !important;margin-top:2px", "margin-bottom: 1px !important; margin-left: 1px !important; margin-right: 1px !important; margin-top: 1px !important"), + ("padding:1px 2px;padding-left:9px;padding:3px", "padding-bottom: 3px; padding-left: 3px; padding-right: 3px; padding-top: 3px"), + ("border-radius:4px;border-top-left-radius:9px", "border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-top-left-radius: 9px; border-top-right-radius: 4px"), + ("flex:1 1 auto;flex-grow:5", "flex-basis: auto; flex-grow: 5; flex-shrink: 1"), + ("overflow:hidden;overflow-x:scroll", "overflow-x: scroll; overflow: hidden"), + ("grid-area:a;grid-row:2", "grid-column-end: a; grid-column-start: a; grid-row-end: auto; grid-row-start: 2"), + ("grid-row:2;grid-area:a", "grid-column-end: a; grid-column-start: a; grid-row-end: a; grid-row-start: a"), + ("transition:all .3s;transition-duration:1s", "transition-delay: initial; transition-duration: 1s; transition-property: all; transition-timing-function: initial"), + ("font:bold 14px/1.2 Arial;font-size:20px", "font-family: Arial; font-size: 20px; font-stretch: ; font-style: ; font-variant: ; font-weight: bold; line-height: 1.2"), + ("font-size:20px;font:bold 14px/1.2 Arial", "font-family: Arial; font-size: 14px; font-stretch: ; font-style: ; font-variant: ; font-weight: bold; line-height: 1.2"), + ("border:1px solid red;border-width:3px", "border-bottom-color: rgba(255, 0, 0, 1); border-bottom-style: solid; border-bottom-width: 3px; border-left-color: rgba(255, 0, 0, 1); border-left-style: solid; border-left-width: 3px; border-right-color: rgba(255, 0, 0, 1); border-right-style: solid; border-right-width: 3px; border-top-color: rgba(255, 0, 0, 1); border-top-style: solid; border-top-width: 3px"), + ("border-width:3px;border:1px solid red", "border-bottom-color: rgba(255, 0, 0, 1); border-bottom-style: solid; border-bottom-width: 1px; border-left-color: rgba(255, 0, 0, 1); border-left-style: solid; border-left-width: 1px; border-right-color: rgba(255, 0, 0, 1); border-right-style: solid; border-right-width: 1px; border-top-color: rgba(255, 0, 0, 1); border-top-style: solid; border-top-width: 1px"), + ("background:red;background:blue", "background-attachment: initial; background-clip: initial; background-color: rgba(0, 0, 255, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:red;background-color:blue", "background-attachment: initial; background-clip: initial; background-color: rgba(0, 0, 255, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background-color:blue;background:red", "background-attachment: initial; background-clip: initial; background-color: rgba(255, 0, 0, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:red;background:", "background-attachment: initial; background-clip: initial; background-color: rgba(255, 0, 0, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:linear-gradient(red,blue);background:none", "background-attachment: initial; background-clip: initial; background-color: initial; background-image: none; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + }; + + [Test] + public void RedeclaringAPropertyProducesTheExpectedBlock() + { + var failures = new List(); + + foreach (var (input, expected) in Cases) + { + var actual = Describe(ParseDeclarations(input)); + + if (!String.Equals(actual, expected, StringComparison.Ordinal)) + { + failures.Add($"{input}{Environment.NewLine} expected: {expected}{Environment.NewLine} actual: {actual}"); + } + } + + Assert.That(failures, Is.Empty, String.Join(Environment.NewLine, failures)); + } + + private static String Describe(ICssStyleDeclaration style) + { + var parts = style.Select(p => $"{p.Name}: {p.Value}{(p.IsImportant ? " !important" : String.Empty)}").ToList(); + parts.Sort(StringComparer.Ordinal); + return String.Join("; ", parts); + } + } +} diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b483b2d..1163a28 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -284,7 +284,12 @@ public void SetProperty(String propertyName, String propertyValue, String priori { if (priority is null || priority.Isi(CssKeywords.Important)) { - var property = CreateProperty(propertyName); + // Deliberately not seeded from any existing declaration of the same + // name: assigning Value below always replaces the raw value, so + // reading the old one only costs a lookup. For a shorthand that + // lookup is not even a lookup - it rebuilds the shorthand from its + // longhands just to throw the result away. + var property = _context.CreateProperty(propertyName); if (property is not null) { @@ -372,19 +377,6 @@ private ICssProperty FindUnserializedLonghand(String name, IEnumerable s return null; } - private ICssProperty CreateProperty(String propertyName) - { - var newProperty = _context.CreateProperty(propertyName); - var existing = GetProperty(propertyName); - - if (existing is not null) - { - newProperty.RawValue = existing.RawValue; - } - - return newProperty; - } - private void SetProperty(ICssProperty property) { if (property.IsShorthand)