diff --git a/CHANGELOG.md b/CHANGELOG.md index abc1dd5a..3fd2f7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Released on Saturday, September 12 2026 +- Added a synchronous stylesheet mutation version for CSSOM cache invalidation without DOM mutation records - Improved pseudo-class matching to skip a per-element `ConditionalWeakTable` probe when no state has ever been forced via `SetPseudoClass` # 1.1.2 diff --git a/docs/general/MutationVersion.md b/docs/general/MutationVersion.md new file mode 100644 index 00000000..170cfac6 --- /dev/null +++ b/docs/general/MutationVersion.md @@ -0,0 +1,37 @@ +# CSSOM mutation versions + +A host that caches stylesheet-derived work can read `sheet.GetMutationVersion()` before and after a query. The nullable `Int64` is an opaque equality token. It advances synchronously for native CSSOM writes, including declaration/property edits, selector and condition changes, rule-list edits, sheet media and disabled state. Reading it allocates nothing. A no-op or failed operation that already changed state may advance it; do not use its magnitude or ordering. Read and mutate on the thread that owns the CSSOM. + +Parser construction does not advance the version. Normal synchronous and asynchronous parses finish at +zero; no counter reset is used, so user mutations made in parser callbacks remain visible. Parsing a +rule with an existing sheet as its owner also leaves that sheet's version unchanged. User insertion or +`CssText` replacement publishes the change after installing the parsed result. Parser construction uses +raw rule-list and declaration operations, and the parser's selector/condition initialization does not +call the notifying setters. Hosts must invalidate across parsing/resumption and resource/import loading +boundaries independently; a stylesheet version is not a signal that loading completed. + +This is a separate signal from the document's DOM mutations. For example: + +```csharp +var sheet = (ICssStyleSheet)document.StyleSheets[0]; +var rule = (ICssStyleRule)sheet.Rules[0]; +var version = sheet.GetMutationVersion(); +rule.Style.SetProperty("display", "none"); +// The style element's text and DOM attributes have not changed. +// A DOM MutationObserver receives no record, but the stylesheet version changes. +``` + +The motivating consumer is Jint.Browser's flat layout: an unchanged geometry read should reuse prior work, while a read immediately after a native host CSSOM write must recompute it. An external wrapper only sees writes made through that wrapper. CSSOM implementations are internal and sealed, and existing interfaces offer no synchronous change hook to intercept these native writes. DOM observers cannot report these changes, even if their record queue is drained synchronously. Walking or serializing the CSSOM can detect changes, but repeats work proportional to stylesheet size on every otherwise-unchanged read. The notification therefore belongs at the native mutation points; the cache and layout policy remain outside AngleSharp.Css. + +Track each imported sheet separately. Also track document changes, stylesheet membership/loading, render-device/media environment and host-owned selector state. The version does not claim to version these other inputs. Unsupported stylesheet implementations return `null`; custom rules, selectors, properties or mutable value objects need an explicit invalidation policy or an uncached fallback. The new extension does not add a member to `ICssStyleSheet` or require third-party implementations to change. + +The native-write reproducer was also run against the unmodified `devel` source at `650fb47`, using AngleSharp `1.8.2-beta.715` (which already has the DOM version). A cache keyed by both the DOM version and observer record count still returned the old display value: + +```text +DOM version unchanged: True +DOM markup unchanged: True +Mutation records: 0 +Cached display: block; actual display: none +``` + +`NativeCssomMutationIsInvisibleToADomObserver` keeps this case in the test suite and verifies that the new stylesheet revision allows the cache to refresh. It also checks that computing style does not change the source sheet's version. diff --git a/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs b/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs new file mode 100644 index 00000000..8afb8ff6 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs @@ -0,0 +1,260 @@ +namespace AngleSharp.Css.Tests.Rules +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Dom.Events; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Tests.Mocks; + using AngleSharp.Io; + using AngleSharp.Dom; + using NUnit.Framework; + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using static CssConstructionFunctions; + + [TestFixture] + public class CssMutationVersionTests + { + private static IEnumerable RuleChanges() + { + yield return Change("declaration", "a { display: block }", r => ((ICssStyleRule)r).Style.SetProperty("display", "none")); + yield return Change("remove declaration", "a { display: block }", r => ((ICssStyleRule)r).Style.RemoveProperty("display")); + yield return Change("declaration text", "a { display: block }", r => ((ICssStyleRule)r).Style.CssText = ""); + yield return Change("property value", "a { display: block }", r => ((ICssStyleRule)r).Style.GetProperty("display")!.Value = "none"); + yield return Change("property priority", "a { display: block }", r => ((ICssStyleRule)r).Style.GetProperty("display")!.IsImportant = true); + yield return Change("selector", "a { display: block }", r => ((ICssStyleRule)r).SelectorText = "b"); + yield return Change("rule text", "a { display: block }", r => r.CssText = "b { display: none }"); + yield return Change("nested declaration", "@media screen { a { display: block } }", r => ((ICssStyleRule)((ICssMediaRule)r).Rules[0]).Style.SetProperty("display", "none")); + yield return Change("group insertion", "@media screen {}", r => ((ICssMediaRule)r).Insert("a {}", 0)); + yield return Change("group removal", "@media screen { a {} }", r => ((ICssMediaRule)r).RemoveAt(0)); + yield return Change("group replacement", "@media screen { a {} }", r => r.CssText = "@media print {}"); + yield return Change("media text", "@media screen {}", r => ((ICssMediaRule)r).Media.MediaText = "print"); + yield return Change("media append", "@media screen {}", r => ((ICssMediaRule)r).Media.Add("print")); + yield return Change("media removal", "@media screen {}", r => ((ICssMediaRule)r).Media.Remove("screen")); + yield return Change("supports condition", "@supports (display: block) {}", r => ((ICssSupportsRule)r).ConditionText = "(display: none)"); + yield return Change("container condition", "@container (width > 1px) {}", r => ((ICssContainerRule)r).ConditionText = "(width > 2px)"); + yield return Change("scope", "@scope (.a) {}", r => ((ICssScopeRule)r).ScopeText = "(.b)"); + yield return Change("page selector", "@page :left { margin: 1px }", r => ((ICssPageRule)r).SelectorText = ":right"); + yield return Change("keyframes name", "@keyframes a { from { opacity: 0 } }", r => ((ICssKeyframesRule)r).Name = "b"); + yield return Change("keyframe selector", "@keyframes a { from { opacity: 0 } }", r => ((ICssKeyframeRule)((ICssKeyframesRule)r).Rules[0]).KeyText = "to"); + yield return Change("font declaration", "@font-face { font-family: a }", r => ((ICssFontFaceRule)r).Family = "b"); + yield return Change("descriptor", "@property --x { syntax: '*'; inherits: false }", r => ((ICssPropertyRule)r).SetProperty("inherits", "true")); + yield return Change("descriptor property", "@property --x { syntax: '*'; inherits: false }", r => ((ICssPropertyRule)r).GetProperty("inherits")!.Value = "true"); + } + + private static TestCaseData Change(String name, String css, Action change) => + new TestCaseData(css, change).SetName("MutationVersion: " + name); + + [TestCaseSource(nameof(RuleChanges))] + public void RuleMutationAdvancesVersion(String css, Action change) + { + var sheet = ParseStyleSheet(css); + var before = sheet.GetMutationVersion(); + Assert.AreEqual(0, before, "Constructing a stylesheet must not advance its version."); + + change(sheet.Rules[0]); + + Assert.AreNotEqual(before, sheet.GetMutationVersion()); + var after = sheet.GetMutationVersion(); + Assert.IsNotNull(sheet.ToCss()); + Assert.AreEqual(after, sheet.GetMutationVersion(), "Reading CSS must not invalidate it."); + } + + [TestCase("@charset 'utf-8'; @import 'a.css' screen; @namespace x 'urn:x'; x|a { color: red !important }")] + [TestCase("@document url('https://example.org') { a { color: red } }")] + [TestCase("@supports (display: block) { @container (width > 1px) { a { & b { display: none } } } }")] + [TestCase("@property --x { syntax: '*'; inherits: false } @font-face { font-family: a; src: url(a.woff) }")] + [TestCase("@scope (.a) { a {} } @keyframes a { from { opacity: 0 } } @page :left { margin: 1px }")] + [TestCase("@media screen { a { color: red } } @supports INVALID {} b { color: red; invalid }")] + public async Task SyncAndAsyncConstructionLeaveVersionAtZero(String css) + { + var parser = new CssParser(); + var synchronous = parser.ParseStyleSheet(css); + var asynchronous = await parser.ParseStyleSheetAsync(css).ConfigureAwait(false); + + Assert.Greater(synchronous.Rules.Length, 0); + Assert.AreEqual(0, synchronous.GetMutationVersion()); + Assert.AreEqual(0, asynchronous.GetMutationVersion()); + } + + [TestCase("@media screen { a { color: red } }")] + [TestCase("@font-face { font-family: a }")] + [TestCase("@property --x { syntax: '*'; inherits: false }")] + [TestCase("@supports INVALID {}")] + public void ParsingARuleDoesNotMutateItsOwner(String css) + { + var parser = new CssParser(); + var sheet = parser.ParseStyleSheet("a {}"); + sheet.IsDisabled = true; + var before = sheet.GetMutationVersion(); + + parser.ParseRule(sheet, css); + + Assert.AreEqual(before, sheet.GetMutationVersion()); + Assert.AreEqual(1, sheet.Rules.Length); + } + + [Test] + public void ConstructionDoesNotResetUserMutationsInParseCallbacks() + { + var parser = new CssParser(); + Int64? afterUserMutation = null; + parser.Parsing += (_, ev) => + { + var sheet = ((CssParseEvent)ev).StyleSheet; + Assert.AreEqual(0, sheet.GetMutationVersion()); + sheet.IsDisabled = true; + afterUserMutation = sheet.GetMutationVersion(); + }; + + var result = parser.ParseStyleSheet("@media screen { a { display: block } }"); + + Assert.Greater(afterUserMutation, 0); + Assert.AreEqual(afterUserMutation, result.GetMutationVersion()); + } + + [TestCase(false)] + [TestCase(true)] + public async Task LoadingAStylesheetAndItsImportsLeavesVersionsAtZero(Boolean disabled) + { + var files = new Dictionary { { "child.css", "a { color: red }" } }; + var config = Configuration.Default.With(new TestServerRequester(files)) + .WithDefaultLoader(new LoaderOptions { IsResourceLoadingEnabled = true }).WithCss(); + using var context = BrowsingContext.New(config); + var document = await context.OpenAsync(req => req.Address("http://localhost/index.html") + .Content("")).ConfigureAwait(false); + using var response = new DefaultResponse + { + Address = new Url("http://localhost/parent.css"), + Content = new MemoryStream(Encoding.UTF8.GetBytes("@import 'child.css';")) + }; + var options = new StyleOptions(document) { Element = document.QuerySelector("style"), IsDisabled = disabled }; + + var sheet = (ICssStyleSheet)await new CssStylingService() + .ParseStylesheetAsync(response, options, CancellationToken.None).ConfigureAwait(false); + var imported = ((ICssImportRule)sheet.Rules[0]).Sheet; + + Assert.AreEqual(disabled, sheet.IsDisabled); + Assert.IsNotNull(imported); + Assert.AreEqual(1, imported!.Rules.Length); + Assert.AreEqual(0, sheet.GetMutationVersion()); + Assert.AreEqual(0, imported.GetMutationVersion()); + } + + [Test] + public void SheetMutationsAdvanceOnlyTheirOwnVersion() + { + var first = ParseStyleSheet("a {}"); + var second = ParseStyleSheet("b {}"); + var firstVersion = first.GetMutationVersion(); + var before = second.GetMutationVersion(); + second.Insert("c {}", 1); + Assert.AreNotEqual(before, second.GetMutationVersion()); + before = second.GetMutationVersion(); + second.RemoveAt(1); + Assert.AreNotEqual(before, second.GetMutationVersion()); + before = second.GetMutationVersion(); + second.IsDisabled = true; + Assert.AreNotEqual(before, second.GetMutationVersion()); + before = second.GetMutationVersion(); + second.Media.MediaText = "print"; + Assert.AreNotEqual(before, second.GetMutationVersion()); + Assert.AreEqual(firstVersion, first.GetMutationVersion()); + } + + [Test] + public void RevisionAdvancesBeforeDeclarationCallbacks() + { + var sheet = ParseStyleSheet("a { display: block }"); + var style = (CssStyleDeclaration)((ICssStyleRule)sheet.Rules[0]).Style; + var before = sheet.GetMutationVersion(); + var observed = before; + style.Changed += _ => observed = sheet.GetMutationVersion(); + + style.SetProperty("display", "none"); + + Assert.AreNotEqual(before, observed); + } + + [Test] + public void ReentrantDeclarationMutationAdvancesVersionWithoutRepeatingCallbacks() + { + var sheet = ParseStyleSheet("a { display: block }"); + var style = (CssStyleDeclaration)((ICssStyleRule)sheet.Rules[0]).Style; + var callbacks = 0; + style.Changed += _ => + { + callbacks++; + var before = sheet.GetMutationVersion(); + style.SetProperty("color", "red"); + Assert.AreNotEqual(before, sheet.GetMutationVersion()); + }; + + style.SetProperty("display", "none"); + + Assert.AreEqual(1, callbacks); + } + + [Test] + public void InvalidMediaThatClearsStateStillAdvancesVersion() + { + var sheet = ParseStyleSheet("a {}"); + sheet.Media.MediaText = "screen"; + var before = sheet.GetMutationVersion(); + + Assert.Throws(() => sheet.Media.MediaText = "@"); + + Assert.AreNotEqual(before, sheet.GetMutationVersion()); + } + + [Test] + public void DetachedGroupUsesItsNewSheet() + { + var first = ParseStyleSheet("@media screen { a { display: block } }"); + var group = first.Rules[0]; + var second = ParseStyleSheet(""); + first.RemoveAt(0); + ((CssStyleSheet)second).Add(group); + var firstVersion = first.GetMutationVersion(); + var secondVersion = second.GetMutationVersion(); + + ((ICssStyleRule)((ICssMediaRule)group).Rules[0]).Style.SetProperty("display", "none"); + + Assert.AreEqual(firstVersion, first.GetMutationVersion()); + Assert.AreNotEqual(secondVersion, second.GetMutationVersion()); + } + + [Test] + public async Task NativeCssomMutationIsInvisibleToADomObserver() + { + using var context = BrowsingContext.New(Configuration.Default.WithCss()); + var document = await context.OpenAsync(req => req.Content("
")).ConfigureAwait(false); + var sheet = (ICssStyleSheet)document.StyleSheets[0]!; + Assert.AreEqual(0, sheet.GetMutationVersion()); + var rule = (ICssStyleRule)sheet.Rules[0]; + var markup = document.DocumentElement.OuterHtml; + var records = 0; + var observer = new MutationObserver((changes, _) => records += changes.Length); + observer.Connect(document, childList: true, subtree: true, attributes: true, characterData: true); + var version = sheet.GetMutationVersion(); + var cachedDisplay = rule.Style.GetPropertyValue("display"); + + // Native CSSOM writes do not rewrite the style element's text or pass through a host wrapper. + rule.Style.SetProperty("display", "none"); + if (version != sheet.GetMutationVersion()) + { + cachedDisplay = rule.Style.GetPropertyValue("display"); + } + + Assert.AreEqual(markup, document.DocumentElement.OuterHtml); + Assert.AreEqual(0, records); + Assert.AreEqual("none", cachedDisplay); + var after = sheet.GetMutationVersion(); + Assert.AreEqual("none", document.DefaultView!.GetComputedStyle(document.QuerySelector("div")!).GetPropertyValue("display")); + Assert.AreEqual(after, sheet.GetMutationVersion(), "Computing style must not invalidate the source sheet."); + } + } +} diff --git a/src/AngleSharp.Css/CssStylingService.cs b/src/AngleSharp.Css/CssStylingService.cs index 4bea528e..53f33259 100644 --- a/src/AngleSharp.Css/CssStylingService.cs +++ b/src/AngleSharp.Css/CssStylingService.cs @@ -37,9 +37,8 @@ public async Task ParseStylesheetAsync(IResponse response, StyleOpt var parser = context.GetService(); var url = response.Address?.Href; var source = new TextSource(response.Content); - var sheet = new CssStyleSheet(context, source) + var sheet = new CssStyleSheet(context, source, options.IsDisabled) { - IsDisabled = options.IsDisabled, Href = url }; sheet.SetOwner(options.Element); diff --git a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs index f9241baf..30d306a3 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs @@ -70,6 +70,18 @@ public String Value } } + String ICssProperty.Value + { + get => Value; + set { Value = value; MutationOwner?.MarkChanged(); } + } + + Boolean ICssProperty.IsImportant + { + get => IsImportant; + set { IsImportant = value; MutationOwner?.MarkChanged(); } + } + public Boolean HasValue => _value != null; public PropertyFlags Flags => _flags; @@ -98,6 +110,8 @@ public Boolean IsImportant #region Internal Properties + internal ICssMutationTracker MutationOwner { get; set; } + internal Boolean CanBeHashless => (_flags & PropertyFlags.Hashless) == PropertyFlags.Hashless; internal Boolean CanBeUnitless => (_flags & PropertyFlags.Unitless) == PropertyFlags.Unitless; diff --git a/src/AngleSharp.Css/Dom/Internal/CssRule.cs b/src/AngleSharp.Css/Dom/Internal/CssRule.cs index 21360603..a4ebed96 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssRule.cs @@ -9,7 +9,7 @@ namespace AngleSharp.Css.Dom /// /// Represents a CSS rule. /// - abstract class CssRule : ICssRule + abstract class CssRule : ICssRule, ICssMutationTracker { #region Fields @@ -46,6 +46,7 @@ public String CssText throw new DomException(DomError.InvalidModification); ReplaceWith(rule); + MarkChanged(); } } @@ -95,6 +96,19 @@ public void SetOwner(ICssStyleSheet sheet) #region Helpers + public void MarkChanged() + { + // Resolve through the current parent: a detached group may later join another sheet. + if (_parent is CssRule parent) + { + parent.MarkChanged(); + } + else if (_owner is CssStyleSheet sheet) + { + sheet.MarkChanged(); + } + } + protected abstract void ReplaceWith(ICssRule rule); protected ISelector ParseSelector(String selectorText) diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b264d3b9..6ef06354 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -14,7 +14,7 @@ namespace AngleSharp.Css.Dom /// /// Represents a single CSS declaration block. /// - sealed class CssStyleDeclaration : ICssStyleDeclaration + sealed class CssStyleDeclaration : ICssStyleDeclaration, ICssMutationTracker, ICssDeclarationBuilder { #region Fields @@ -120,7 +120,12 @@ public void Update(String value) if (decl != null) { - _declarations.AddRange(decl); + foreach (var property in decl) + { + Track(property); + _declarations.Add(property); + } + RebuildIndex(); } } @@ -285,6 +290,17 @@ public void SetPropertyPriority(String propertyName, String priority) } public void SetProperty(String propertyName, String propertyValue, String priority = null) + { + if (SetPropertyCore(propertyName, propertyValue, priority)) + { + RaiseChanged(); + } + } + + void ICssDeclarationBuilder.SetProperty(String name, String value, String priority) => + SetPropertyCore(name, value, priority); + + private Boolean SetPropertyCore(String propertyName, String propertyValue, String priority) { if (IsReadOnly) throw new DomException(DomError.NoModificationAllowed); @@ -308,19 +324,23 @@ public void SetProperty(String propertyName, String propertyValue, String priori { property.IsImportant = priority is not null; SetProperty(property); - RaiseChanged(); + return true; } } } } else { - RemoveProperty(propertyName); + RemovePropertyByName(propertyName); + return true; } + + return false; } public void AddProperty(ICssProperty declaration) { + Track(declaration); _declarationIndex[declaration.Name] = _declarations.Count; _declarations.Add(declaration); } @@ -482,6 +502,7 @@ private void ChangeDeclarations(IEnumerable decls, Predicate decls, Predicate (_parent as CssRule)?.MarkChanged(); + + private void Track(ICssProperty property) + { + if (_parent is CssRule && property is CssProperty css) + { + css.MutationOwner = this; + } + } + private void RaiseChanged() { + MarkChanged(); + if (!_updating) { _updating = true; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs index 422949ff..48eda23a 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs @@ -12,7 +12,7 @@ namespace AngleSharp.Css.Dom /// /// Represents a CSS Stylesheet. /// - sealed class CssStyleSheet : ICssStyleSheet + sealed class CssStyleSheet : ICssStyleSheet, ICssMutationTracker { #region Fields @@ -23,6 +23,8 @@ sealed class CssStyleSheet : ICssStyleSheet private ICssStyleSheet _parent; private ICssRule _owner; private IElement _element; + private Int64 _mutationVersion; + private Boolean _disabled; #endregion @@ -32,10 +34,16 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) { _context = context; _source = source; - _media = new MediaList(context); + _media = new MediaList(context, this); _rules = new CssRuleList(); } + internal CssStyleSheet(IBrowsingContext context, TextSource source, Boolean disabled) + : this(context, source) + { + _disabled = disabled; + } + #endregion #region Properties @@ -54,10 +62,21 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) public Boolean IsDisabled { - get; - set; + get => _disabled; + set + { + if (_disabled != value) + { + _disabled = value; + MarkChanged(); + } + } } + internal Int64 MutationVersion => _mutationVersion; + + public void MarkChanged() => _mutationVersion++; + public IElement OwnerNode => _element; public ICssStyleSheet Parent => _parent; @@ -88,6 +107,7 @@ public void Add(ICssRule rule) public void Remove(ICssRule rule) { _rules.Remove(rule); + MarkChanged(); rule.SetOwner(null); } @@ -105,6 +125,7 @@ public Int32 Insert(String ruleText, Int32 index) var parser = _context.GetService(); var rule = parser.ParseRule(this, ruleText); _rules.Insert(index, rule); + MarkChanged(); rule.SetOwner(this); return index; } diff --git a/src/AngleSharp.Css/Dom/Internal/ICssDeclarationBuilder.cs b/src/AngleSharp.Css/Dom/Internal/ICssDeclarationBuilder.cs new file mode 100644 index 00000000..f53ad6c1 --- /dev/null +++ b/src/AngleSharp.Css/Dom/Internal/ICssDeclarationBuilder.cs @@ -0,0 +1,12 @@ +namespace AngleSharp.Css.Dom +{ + using System; + + /// + /// Declaration construction without CSSOM mutation notifications. + /// + interface ICssDeclarationBuilder + { + void SetProperty(String name, String value, String priority); + } +} diff --git a/src/AngleSharp.Css/Dom/Internal/ICssMutationTracker.cs b/src/AngleSharp.Css/Dom/Internal/ICssMutationTracker.cs new file mode 100644 index 00000000..4e3b0609 --- /dev/null +++ b/src/AngleSharp.Css/Dom/Internal/ICssMutationTracker.cs @@ -0,0 +1,8 @@ +namespace AngleSharp.Css.Dom +{ + // Shared by sheets and rules so lists can notify their current owner without callbacks. + internal interface ICssMutationTracker + { + void MarkChanged(); + } +} diff --git a/src/AngleSharp.Css/Dom/Internal/MediaList.cs b/src/AngleSharp.Css/Dom/Internal/MediaList.cs index d13e328a..53b3752f 100644 --- a/src/AngleSharp.Css/Dom/Internal/MediaList.cs +++ b/src/AngleSharp.Css/Dom/Internal/MediaList.cs @@ -17,6 +17,7 @@ sealed class MediaList : IMediaList #region Fields private readonly IBrowsingContext _context; + private readonly ICssMutationTracker _owner; private readonly List _media; private static readonly CssMedium replacementMedium = new(CssKeywords.All, inverse: true, exclusive: false); @@ -25,9 +26,10 @@ sealed class MediaList : IMediaList #region ctor - internal MediaList(IBrowsingContext context) + internal MediaList(IBrowsingContext context, ICssMutationTracker owner = null) { _context = context; + _owner = owner; _media = new List(); } @@ -50,7 +52,18 @@ internal MediaList(IBrowsingContext context) public String MediaText { get => this.ToCss(); - set => SetMediaText(value, throwOnError: true); + set + { + // Core initializes an attached sheet's empty media through this public setter. + if (_media.Count == 0 && String.IsNullOrEmpty(value)) + { + return; + } + + _media.Clear(); + _owner?.MarkChanged(); + FillMediaText(value, throwOnError: true); + } } #endregion @@ -60,6 +73,11 @@ public String MediaText public void SetMediaText(String value, Boolean throwOnError) { _media.Clear(); + FillMediaText(value, throwOnError); + } + + private void FillMediaText(String value, Boolean throwOnError) + { var v = String.IsNullOrEmpty(value) ? String.Empty : value; var media = MediaParser.Parse(v, ValidatorFactory) ?? Enumerable.Repeat(null, 1); @@ -75,6 +93,7 @@ public void Add(String newMedium) { var medium = MediumParser.Parse(newMedium, ValidatorFactory) ?? throw new DomException(DomError.Syntax); _media.Add(medium); + _owner?.MarkChanged(); } public void Remove(String oldMedium) @@ -86,6 +105,7 @@ public void Remove(String oldMedium) if (_media[i].Equals(medium)) { _media.RemoveAt(i); + _owner?.MarkChanged(); return; } } @@ -96,6 +116,7 @@ public void Remove(String oldMedium) public void Replace(IEnumerable media) { _media.Clear(); + _owner?.MarkChanged(); _media.AddRange(media); } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs index 82373c92..945c1309 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs @@ -32,13 +32,15 @@ internal CssCharsetRule(ICssStyleSheet sheet) public String CharacterSet { get => _charSet; - set => _charSet = value ?? String.Empty; + set { InitializeCharacterSet(value); MarkChanged(); } } #endregion #region Methods + internal void InitializeCharacterSet(String value) => _charSet = value ?? String.Empty; + protected override void ReplaceWith(ICssRule rule) { _charSet = ((ICssCharsetRule)rule).CharacterSet; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs index 792c1267..67040060 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs @@ -48,7 +48,13 @@ public String ConditionText return String.Concat(_containerName, " ", _containerQuery); } - set => SetConditionText(value, throwOnError: true); + set + { + if (SetConditionText(value, throwOnError: true)) + { + MarkChanged(); + } + } } public String ContainerName => _containerName ?? String.Empty; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs index 9d252016..bbfa93d1 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs @@ -11,7 +11,7 @@ namespace AngleSharp.Css.Dom /// /// Represents the base class for all style-rule similar rules. /// - abstract class CssDeclarationRule : CssRule, ICssProperties + abstract class CssDeclarationRule : CssRule, ICssProperties, ICssDeclarationBuilder { #region Fields @@ -62,6 +62,7 @@ public String RemoveProperty(String propertyName) if (declaration.Name.Is(propertyName)) { _declarations.RemoveAt(i); + MarkChanged(); return declaration.Value; } } @@ -83,7 +84,7 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) #region Helpers - private ICssProperty CreateNewProperty(String propertyName) + private CssProperty CreateNewProperty(String propertyName) { // Descriptors of the rule itself are always created. Anything else is // kept only when unknown declarations are included - the same switch @@ -111,6 +112,15 @@ protected String GetValue(String propertyName) } protected void SetValue(String propertyName, String valueText) + { + InitializeValue(propertyName, valueText); + MarkChanged(); + } + + void ICssDeclarationBuilder.SetProperty(String name, String value, String priority) => + InitializeValue(name, value); + + private void InitializeValue(String propertyName, String valueText) { if (!String.IsNullOrEmpty(valueText)) { @@ -130,6 +140,11 @@ protected void SetValue(String propertyName, String valueText) return; } + if (property is CssProperty css) + { + css.MutationOwner = this; + } + for (var i = 0; i < _declarations.Count; i++) { if (_declarations[i].Name.Is(propertyName)) diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs index 8927351d..54a80c16 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs @@ -9,7 +9,7 @@ namespace AngleSharp.Css.Dom using System.Collections.Generic; using System.IO; - abstract class CssDescriptorRule : CssRule, ICssProperties + abstract class CssDescriptorRule : CssRule, ICssProperties, ICssDeclarationBuilder { private readonly List _descriptors; private readonly String _ruleName; @@ -55,6 +55,15 @@ public String GetPropertyPriority(String propertyName) } public void SetProperty(String propertyName, String propertyValue, String priority = null) + { + InitializeProperty(propertyName, propertyValue, priority); + MarkChanged(); + } + + void ICssDeclarationBuilder.SetProperty(String name, String value, String priority) => + InitializeProperty(name, value, priority); + + private void InitializeProperty(String propertyName, String propertyValue, String priority) { if (String.IsNullOrEmpty(propertyName)) { @@ -75,7 +84,7 @@ public void SetProperty(String propertyName, String propertyValue, String priori if (property is null) { - property = new DescriptorProperty(propertyName); + property = new DescriptorProperty(propertyName, this); _descriptors.Add(property); } @@ -92,6 +101,7 @@ public String RemoveProperty(String propertyName) if (descriptor.Name.Is(propertyName)) { _descriptors.RemoveAt(i); + MarkChanged(); return descriptor.Value; } } @@ -128,19 +138,27 @@ protected void ReplaceWith(ICssProperties properties) private sealed class DescriptorProperty : ICssProperty { private readonly String _name; + private readonly CssRule _owner; + private String _value = String.Empty; + private Boolean _important; - public DescriptorProperty(String name) + public DescriptorProperty(String name, CssRule owner) { _name = name; + _owner = owner; } public String Name => _name; public ICssValue RawValue => null; - public String Value { get; set; } = String.Empty; + public String Value { get => _value; set => _value = value; } + + String ICssProperty.Value { get => Value; set { Value = value; _owner.MarkChanged(); } } + + public Boolean IsImportant { get => _important; set => _important = value; } - public Boolean IsImportant { get; set; } + Boolean ICssProperty.IsImportant { get => IsImportant; set { IsImportant = value; _owner.MarkChanged(); } } public Boolean IsInherited => false; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs index 594143da..ad0e1756 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs @@ -34,7 +34,18 @@ internal CssDocumentRule(ICssStyleSheet owner) public String ConditionText { get => _conditions.ToCss(); - set => SetConditionText(value, throwOnError: true); + set + { + try + { + SetConditionText(value, throwOnError: true); + } + finally + { + // @document may clear its conditions before throwing. + MarkChanged(); + } + } } public IDocumentFunctions Conditions => _conditions; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs index f398a59b..c71c5a6f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs @@ -53,6 +53,7 @@ public Int32 Insert(String ruleText, Int32 index) { var rule = Parser.ParseRule(Owner, ruleText); _rules.Insert(index, rule); + MarkChanged(); rule.SetParent(this); return index; } @@ -75,6 +76,7 @@ public void Add(ICssRule rule) public void Remove(ICssRule rule) { _rules.Remove(rule); + MarkChanged(); rule.SetParent(null); } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs index d2159795..57d39a2f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs @@ -24,7 +24,7 @@ sealed class CssImportRule : CssRule, ICssImportRule internal CssImportRule(ICssStyleSheet owner) : base(owner, CssRuleType.Import) { - _media = new MediaList(owner.Context); + _media = new MediaList(owner.Context, this); } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs index 95eda6c9..145d87f9 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs @@ -34,7 +34,7 @@ internal CssKeyframeRule(ICssStyleSheet owner) public String KeyText { get => _selector?.ToCss(); - set => _selector = KeyframeParser.Parse(value); + set { InitializeKeyText(value); MarkChanged(); } } public IKeyframeSelector Key => _selector; @@ -47,6 +47,8 @@ public String KeyText #region Methods + internal void InitializeKeyText(String value) => _selector = KeyframeParser.Parse(value); + protected override void ReplaceWith(ICssRule rule) { var newRule = (ICssKeyframeRule)rule; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs index c50cb30a..09d1893e 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs @@ -33,7 +33,7 @@ internal CssKeyframesRule(ICssStyleSheet owner) public String Name { get => _name; - set => _name = value; + set { InitializeName(value); MarkChanged(); } } #endregion @@ -44,6 +44,7 @@ public void Add(String ruleText) { var rule = Parser.ParseKeyframeRule(Owner, ruleText); Add(rule); + MarkChanged(); } public void Remove(String key) @@ -57,6 +58,8 @@ public ICssKeyframeRule Find(String key) return Rules.OfType().FirstOrDefault(m => key.Isi(m.KeyText)); } + internal void InitializeName(String value) => _name = value; + protected override void ReplaceWith(ICssRule rule) { var newRule = (ICssKeyframesRule)rule; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssMediaRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssMediaRule.cs index 5814ef85..4ae68743 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssMediaRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssMediaRule.cs @@ -21,7 +21,7 @@ sealed class CssMediaRule : CssConditionRule, ICssMediaRule internal CssMediaRule(ICssStyleSheet owner) : base(owner, CssRuleType.Media) { - _media = new MediaList(owner.Context); + _media = new MediaList(owner.Context, this); } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs index ad1f2dcc..c6040182 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs @@ -34,19 +34,31 @@ internal CssNamespaceRule(ICssStyleSheet owner) public String NamespaceUri { get => _namespaceUri; - set { CheckValidity(); _namespaceUri = value ?? String.Empty; } + set { InitializeNamespaceUri(value); MarkChanged(); } } public String Prefix { get => _prefix; - set { CheckValidity(); _prefix = value ?? String.Empty; } + set { InitializePrefix(value); MarkChanged(); } } #endregion #region Methods + internal void InitializePrefix(String value) + { + CheckValidity(); + _prefix = value ?? String.Empty; + } + + internal void InitializeNamespaceUri(String value) + { + CheckValidity(); + _namespaceUri = value ?? String.Empty; + } + protected override void ReplaceWith(ICssRule rule) { var newRule = (ICssNamespaceRule)rule; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs index 21d92c0f..3db77ff0 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs @@ -34,7 +34,7 @@ internal CssPageRule(ICssStyleSheet owner) public String SelectorText { get => _selector?.Text; - set { _selector = ParseSelector(value); } + set { InitializeSelectorText(value); MarkChanged(); } } public ISelector Selector => _selector; @@ -52,6 +52,8 @@ internal void SetInvalidSelector(String selectorText) _selector = new InvalidSelector(selectorText); } + internal void InitializeSelectorText(String value) => _selector = ParseSelector(value); + protected override void ReplaceWith(ICssRule rule) { var newRule = (ICssPageRule)rule; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs index c44bda3a..1a966776 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs @@ -16,7 +16,15 @@ internal CssScopeRule(ICssStyleSheet owner) { } - public String ScopeText { get; set; } + private String _scopeText; + + public String ScopeText + { + get => _scopeText; + set { InitializeScopeText(value); MarkChanged(); } + } + + internal void InitializeScopeText(String value) => _scopeText = value; protected override void ReplaceWith(ICssRule rule) { diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs index 09d5d83d..b74c5f75 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs @@ -55,7 +55,7 @@ public ISelector Selector public String SelectorText { get => _selector?.Text; - set => ChangeSelector(ParseSelector(value)); + set { InitializeSelectorText(value); MarkChanged(); } } ICssStyleDeclaration ICssStyleRule.Style => _style; @@ -154,6 +154,8 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) #region Selector + internal void InitializeSelectorText(String value) => ChangeSelector(ParseSelector(value)); + internal void ChangeSelector(ISelector value) { _selectorList = null; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs index 14fc5ed9..9b9e07c9 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs @@ -33,7 +33,13 @@ internal CssSupportsRule(ICssStyleSheet owner) public String ConditionText { get => _condition.ToCss(); - set => SetConditionText(value, throwOnError: true); + set + { + if (SetConditionText(value, throwOnError: true)) + { + MarkChanged(); + } + } } public IConditionFunction Condition => _condition; diff --git a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs index 579ccdd1..2fdd0020 100644 --- a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs +++ b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs @@ -12,6 +12,20 @@ namespace AngleSharp.Dom /// public static class StyleSheetExtensions { + /// + /// Gets the synchronous CSSOM mutation version of an AngleSharp stylesheet. + /// + /// The stylesheet to inspect. + /// An opaque version, or null for an unsupported implementation. + /// + /// Compare readings for equality, on the owning thread. No-op writes may advance the + /// version. Parser construction leaves it unchanged; invalidate at parser and resource-loading + /// boundaries separately. Track imported sheets, document and render-device state independently. + /// Custom rules, selectors, properties and value objects need their own invalidation signal. + /// + public static Int64? GetMutationVersion(this ICssStyleSheet sheet) => + sheet is CssStyleSheet css ? css.MutationVersion : null; + /// /// Gets all rules that are of the provided type. /// diff --git a/src/AngleSharp.Css/Parser/CssBuilder.cs b/src/AngleSharp.Css/Parser/CssBuilder.cs index a798a690..66e14c72 100644 --- a/src/AngleSharp.Css/Parser/CssBuilder.cs +++ b/src/AngleSharp.Css/Parser/CssBuilder.cs @@ -139,7 +139,7 @@ private CssCharsetRule CreateCharset(CssCharsetRule rule, CssToken current) if (token.Type == CssTokenType.String) { - rule.CharacterSet = token.Data; + rule.InitializeCharacterSet(token.Data); } JumpToEnd(ref token); @@ -226,7 +226,7 @@ private CssKeyframesRule CreateKeyframes(CssKeyframesRule rule, CssToken current { var token = NextToken(); CollectTrivia(rule.Owner, ref token); - rule.Name = GetRuleName(ref token); + rule.InitializeName(GetRuleName(ref token)); CollectTrivia(rule.Owner, ref token); if (token.Type != CssTokenType.CurlyBracketOpen) @@ -265,7 +265,7 @@ private CssNamespaceRule CreateNamespace(CssNamespaceRule rule, CssToken current { var token = NextToken(); CollectTrivia(rule.Owner, ref token); - rule.Prefix = GetRuleName(ref token); + rule.InitializePrefix(GetRuleName(ref token)); CollectTrivia(rule.Owner, ref token); if (!token.Is(CssTokenType.String, CssTokenType.Url)) @@ -275,7 +275,7 @@ private CssNamespaceRule CreateNamespace(CssNamespaceRule rule, CssToken current return null; } - rule.NamespaceUri = token.Data; + rule.InitializeNamespaceUri(token.Data); JumpToEnd(ref token); return rule; } @@ -285,7 +285,7 @@ private CssPageRule CreatePage(CssPageRule rule, CssToken current) current = NextToken(); var selectorText = GetArgument(ref current); - rule.SelectorText = selectorText; + rule.InitializeSelectorText(selectorText); if (rule.Selector is null && _options.IsToleratingInvalidSelectors) { @@ -403,7 +403,7 @@ private CssScopeRule CreateScope(CssScopeRule rule, CssToken current) { var token = NextToken(); CollectTrivia(rule.Owner, ref token); - rule.ScopeText = GetArgument(ref token); + rule.InitializeScopeText(GetArgument(ref token)); CollectTrivia(rule.Owner, ref token); if (token.Type != CssTokenType.CurlyBracketOpen) @@ -519,7 +519,7 @@ public CssStyleRule CreateStyle(CssStyleRule rule, CssToken current) CollectTrivia(rule.Owner, ref current); var selectorText = GetArgument(ref current); - rule.SelectorText = selectorText; + rule.InitializeSelectorText(selectorText); if (rule.Selector is null && _options.IsToleratingInvalidSelectors) { @@ -540,7 +540,7 @@ public CssKeyframeRule CreateKeyframeRule(CssKeyframeRule rule, CssToken current { CollectTrivia(rule.Owner, ref current); var position = current.Position; - rule.KeyText = GetArgument(ref current); + rule.InitializeKeyText(GetArgument(ref current)); if (rule.Key is null) { @@ -726,7 +726,7 @@ public CssStyleDeclaration FillDeclarations(ICssStyleSheet owner, CssStyleDeclar /// /// Called before the property name has been detected. /// - public void CreateDeclarationWith(ICssStyleSheet owner, ICssProperties properties, ref CssToken token) + public void CreateDeclarationWith(ICssStyleSheet owner, ICssDeclarationBuilder properties, ref CssToken token) { CollectTrivia(owner, ref token); var start = token.Position; diff --git a/src/AngleSharp.Css/Parser/CssParser.cs b/src/AngleSharp.Css/Parser/CssParser.cs index eb1dd81b..ab756b30 100644 --- a/src/AngleSharp.Css/Parser/CssParser.cs +++ b/src/AngleSharp.Css/Parser/CssParser.cs @@ -277,7 +277,6 @@ private async Task ParseChildStyleSheetAsync(IDownload download, var source = new TextSource(response.Content); var sheet = new CssStyleSheet(_context, source) { - IsDisabled = false, Href = url }; sheet.SetParent(parent); diff --git a/src/AngleSharp.Performance.Css/CssMutationVersionBenchmark.cs b/src/AngleSharp.Performance.Css/CssMutationVersionBenchmark.cs new file mode 100644 index 00000000..b15a1c4f --- /dev/null +++ b/src/AngleSharp.Performance.Css/CssMutationVersionBenchmark.cs @@ -0,0 +1,59 @@ +namespace AngleSharp.Performance.Css +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Dom; + using BenchmarkDotNet.Attributes; + using System; + + /// + /// Measures native CSSOM writes and the parse/read controls with an isolated sheet per process. + /// Parsing a new sheet is measured only by ParseSheet; other cases reuse setup state. + /// + [MemoryDiagnoser] + public class CssMutationVersionBenchmark + { + private const String Source = "@media screen { div { display: block; width: 10px; color: red } } a { margin: 0; padding: 2px }"; + private CssParser _parser = default!; + private ICssStyleSheet _sheet = default!; + private ICssStyleDeclaration _style = default!; + private ICssMediaRule _media = default!; + private Boolean _toggle; + + [GlobalSetup] + public void Setup() + { + _parser = new CssParser(); + _sheet = _parser.ParseStyleSheet(Source); + _media = (ICssMediaRule)_sheet.Rules[0]; + _style = ((ICssStyleRule)_media.Rules[0]).Style; + } + + [Benchmark] + public ICssStyleSheet ParseSheet() => _parser.ParseStyleSheet(Source); + + [Benchmark] + public String ReadProperty() => _style.GetPropertyValue("display"); + + [Benchmark] + public void SetProperty() + { + _toggle = !_toggle; + _style.SetProperty("display", _toggle ? "block" : "none"); + } + + [Benchmark] + public void SetMedia() + { + _toggle = !_toggle; + _media.Media.MediaText = _toggle ? "screen" : "print"; + } + + [Benchmark] + public void InsertRemoveRule() + { + _sheet.Insert("b { display: none }", 2); + _sheet.RemoveAt(2); + } + } +}