From 5c2f66bfc5a7bb67ae0f9fa8cac3423c46b1c6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= <1165805+sebastienros@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:14:32 -0700 Subject: [PATCH 1/2] Expose synchronous native CSSOM mutation versions --- CHANGELOG.md | 1 + docs/general/MutationVersion.md | 29 ++++ .../Rules/CssMutationVersion.cs | 152 ++++++++++++++++++ .../Dom/Internal/CssProperty.cs | 6 +- src/AngleSharp.Css/Dom/Internal/CssRule.cs | 16 +- .../Dom/Internal/CssRuleList.cs | 10 +- .../Dom/Internal/CssStyleDeclaration.cs | 23 ++- .../Dom/Internal/CssStyleSheet.cs | 16 +- .../Dom/Internal/ICssMutationTracker.cs | 8 + src/AngleSharp.Css/Dom/Internal/MediaList.cs | 8 +- .../Dom/Internal/Rules/CssCharsetRule.cs | 2 +- .../Dom/Internal/Rules/CssContainerRule.cs | 1 + .../Dom/Internal/Rules/CssDeclarationRule.cs | 8 + .../Dom/Internal/Rules/CssDescriptorRule.cs | 14 +- .../Dom/Internal/Rules/CssDocumentRule.cs | 2 + .../Dom/Internal/Rules/CssGroupingRule.cs | 2 +- .../Dom/Internal/Rules/CssImportRule.cs | 4 +- .../Dom/Internal/Rules/CssKeyframeRule.cs | 2 +- .../Dom/Internal/Rules/CssKeyframesRule.cs | 2 +- .../Dom/Internal/Rules/CssMediaRule.cs | 2 +- .../Dom/Internal/Rules/CssNamespaceRule.cs | 4 +- .../Dom/Internal/Rules/CssPageRule.cs | 2 +- .../Dom/Internal/Rules/CssScopeRule.cs | 8 +- .../Dom/Internal/Rules/CssStyleRule.cs | 3 +- .../Dom/Internal/Rules/CssSupportsRule.cs | 1 + .../Extensions/StyleSheetExtensions.cs | 13 ++ .../CssMutationVersionBenchmark.cs | 59 +++++++ 27 files changed, 371 insertions(+), 27 deletions(-) create mode 100644 docs/general/MutationVersion.md create mode 100644 src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs create mode 100644 src/AngleSharp.Css/Dom/Internal/ICssMutationTracker.cs create mode 100644 src/AngleSharp.Performance.Css/CssMutationVersionBenchmark.cs 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..8467784b --- /dev/null +++ b/docs/general/MutationVersion.md @@ -0,0 +1,29 @@ +# 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. + +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..777d7fd3 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs @@ -0,0 +1,152 @@ +namespace AngleSharp.Css.Tests.Rules +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using NUnit.Framework; + using System; + using System.Collections.Generic; + 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.IsNotNull(before); + + 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."); + } + + [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 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]!; + 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/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs index f9241baf..2cbcc46a 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs @@ -62,11 +62,13 @@ public String Value if (parsed != null && source.IsDone && parsed is not CssVarValue) { _value = parsed; + MutationOwner?.MarkChanged(); return; } } _value = _converter.Convert(value); + MutationOwner?.MarkChanged(); } } @@ -89,7 +91,7 @@ public String Value public Boolean IsImportant { get => _important; - set => _important = value; + set { _important = value; MutationOwner?.MarkChanged(); } } public String CssText => this.ToCss(); @@ -98,6 +100,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/CssRuleList.cs b/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs index b2175856..83240a30 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs @@ -13,14 +13,16 @@ sealed class CssRuleList : ICssRuleList #region Fields private readonly List _rules; + private readonly ICssMutationTracker _owner; #endregion #region ctor - internal CssRuleList() + internal CssRuleList(ICssMutationTracker owner) { _rules = new List(); + _owner = owner; } #endregion @@ -74,6 +76,7 @@ public Int32 Length public void Clear() { _rules.Clear(); + _owner.MarkChanged(); } public void RemoveAt(Int32 index) @@ -98,6 +101,7 @@ public void Remove(ICssRule rule) if (index >= 0) { _rules.RemoveAt(index); + _owner.MarkChanged(); } } } @@ -121,10 +125,12 @@ public void Insert(Int32 index, ICssRule rule) if (actualIndex == _rules.Count) { _rules.Add(rule); + _owner.MarkChanged(); } else { _rules.Insert(actualIndex, rule); + _owner.MarkChanged(); } } @@ -133,6 +139,7 @@ public void Add(ICssRule rule) if (rule != null) { _rules.Add(rule); + _owner.MarkChanged(); } } @@ -145,6 +152,7 @@ public void AddRange(IEnumerable rules) var oldLength = _rules.Count; _rules.AddRange(rules); + _owner.MarkChanged(); } internal IEnumerable GetFormattables() => _rules; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b264d3b9..56514e9e 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 { #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(); } } @@ -321,6 +326,7 @@ public void SetProperty(String propertyName, String propertyValue, String priori public void AddProperty(ICssProperty declaration) { + Track(declaration); _declarationIndex[declaration.Name] = _declarations.Count; _declarations.Add(declaration); } @@ -482,6 +488,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() { if (!_updating) { + MarkChanged(); _updating = true; Changed?.Invoke(CssText); _updating = false; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs index 422949ff..d0162c58 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,8 +34,8 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) { _context = context; _source = source; - _media = new MediaList(context); - _rules = new CssRuleList(); + _media = new MediaList(context, this); + _rules = new CssRuleList(this); } #endregion @@ -54,10 +56,14 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) public Boolean IsDisabled { - get; - set; + get => _disabled; + set { _disabled = value; MarkChanged(); } } + internal Int64 MutationVersion => _mutationVersion; + + public void MarkChanged() => _mutationVersion++; + public IElement OwnerNode => _element; public ICssStyleSheet Parent => _parent; 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..83dce21e 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(); } @@ -60,6 +62,7 @@ public String MediaText public void SetMediaText(String value, Boolean throwOnError) { _media.Clear(); + _owner?.MarkChanged(); var v = String.IsNullOrEmpty(value) ? String.Empty : value; var media = MediaParser.Parse(v, ValidatorFactory) ?? Enumerable.Repeat(null, 1); @@ -75,6 +78,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 +90,7 @@ public void Remove(String oldMedium) if (_media[i].Equals(medium)) { _media.RemoveAt(i); + _owner?.MarkChanged(); return; } } @@ -96,6 +101,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..bb94381f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs @@ -32,7 +32,7 @@ internal CssCharsetRule(ICssStyleSheet sheet) public String CharacterSet { get => _charSet; - set => _charSet = value ?? String.Empty; + set { _charSet = value ?? String.Empty; MarkChanged(); } } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs index 792c1267..36561b62 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssContainerRule.cs @@ -73,6 +73,7 @@ public Boolean SetConditionText(String value, Boolean throwOnError) _containerName = name; _containerQuery = query; + MarkChanged(); return true; } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs index 9d252016..f7143e44 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs @@ -62,6 +62,7 @@ public String RemoveProperty(String propertyName) if (declaration.Name.Is(propertyName)) { _declarations.RemoveAt(i); + MarkChanged(); return declaration.Value; } } @@ -130,6 +131,13 @@ protected void SetValue(String propertyName, String valueText) return; } + if (property is CssProperty css) + { + css.MutationOwner = this; + } + + MarkChanged(); + 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..4d8f75ab 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDescriptorRule.cs @@ -68,6 +68,7 @@ public void SetProperty(String propertyName, String propertyValue, String priori if (property is not null) { _descriptors.Remove(property); + MarkChanged(); } return; @@ -75,7 +76,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 +93,7 @@ public String RemoveProperty(String propertyName) if (descriptor.Name.Is(propertyName)) { _descriptors.RemoveAt(i); + MarkChanged(); return descriptor.Value; } } @@ -128,19 +130,23 @@ 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; _owner.MarkChanged(); } } - public Boolean IsImportant { get; set; } + public Boolean IsImportant { get => _important; set { _important = 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..a10b0c6d 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDocumentRule.cs @@ -48,6 +48,7 @@ protected override void ReplaceWith(ICssRule rule) base.ReplaceWith(rule); var newRule = (ICssDocumentRule)rule; _conditions.Clear(); + MarkChanged(); _conditions.AddRange(newRule.Conditions); } @@ -56,6 +57,7 @@ public Boolean SetConditionText(String value, Boolean throwOnError) var factory = Owner.Context.GetService(); var conditions = DocumentFunctionParser.Parse(value, factory); _conditions.Clear(); + MarkChanged(); if (conditions != null) { diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs index f398a59b..8091c04f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs @@ -20,7 +20,7 @@ abstract class CssGroupingRule : CssRule, ICssGroupingRule internal CssGroupingRule(ICssStyleSheet owner, CssRuleType type) : base(owner, type) { - _rules = new CssRuleList(); + _rules = new CssRuleList(this); } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs index d2159795..5569ab63 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 @@ -44,7 +44,7 @@ public String Href public ICssStyleSheet Sheet { get => _styleSheet; - set { _styleSheet = value; _styleSheet?.SetParent(Owner); } + set { _styleSheet = value; _styleSheet?.SetParent(Owner); MarkChanged(); } } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs index 95eda6c9..e1afc007 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 { _selector = KeyframeParser.Parse(value); MarkChanged(); } } public IKeyframeSelector Key => _selector; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframesRule.cs index c50cb30a..9cf09019 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 { _name = value; MarkChanged(); } } #endregion 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..4087a822 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs @@ -34,13 +34,13 @@ internal CssNamespaceRule(ICssStyleSheet owner) public String NamespaceUri { get => _namespaceUri; - set { CheckValidity(); _namespaceUri = value ?? String.Empty; } + set { CheckValidity(); _namespaceUri = value ?? String.Empty; MarkChanged(); } } public String Prefix { get => _prefix; - set { CheckValidity(); _prefix = value ?? String.Empty; } + set { CheckValidity(); _prefix = value ?? String.Empty; MarkChanged(); } } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssPageRule.cs index 21d92c0f..b4ebe78b 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 { _selector = ParseSelector(value); MarkChanged(); } } public ISelector Selector => _selector; diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs index c44bda3a..7100f5ed 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs @@ -16,7 +16,13 @@ internal CssScopeRule(ICssStyleSheet owner) { } - public String ScopeText { get; set; } + private String _scopeText; + + public String ScopeText + { + get => _scopeText; + set { _scopeText = value; MarkChanged(); } + } 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..4aac11d4 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs @@ -32,7 +32,7 @@ internal CssStyleRule(ICssStyleSheet owner) : base(owner, CssRuleType.Style) { _style = new CssStyleDeclaration(this); - _rules = new CssRuleList(); + _rules = new CssRuleList(this); _selectorList = null; } @@ -158,6 +158,7 @@ internal void ChangeSelector(ISelector value) { _selectorList = null; _selector = value; + MarkChanged(); value?.Accept(this); } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs index 14fc5ed9..3521d8b3 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs @@ -56,6 +56,7 @@ public Boolean SetConditionText(String value, Boolean throwOnError) } _condition = condition; + MarkChanged(); return true; } diff --git a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs index 579ccdd1..04cd919a 100644 --- a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs +++ b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs @@ -12,6 +12,19 @@ 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. Track imported sheets separately, along with document and render-device state. + /// 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.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); + } + } +} From 53a93e3b699cd86835ad23cf3884c4a947cc167e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= <1165805+sebastienros@users.noreply.github.com> Date: Sun, 13 Sep 2026 12:14:14 -0700 Subject: [PATCH 2/2] Keep CSS parser construction out of mutation notifications --- docs/general/MutationVersion.md | 8 ++ .../Rules/CssMutationVersion.cs | 110 +++++++++++++++++- src/AngleSharp.Css/CssStylingService.cs | 3 +- .../Dom/Internal/CssProperty.cs | 16 ++- .../Dom/Internal/CssRuleList.cs | 10 +- .../Dom/Internal/CssStyleDeclaration.cs | 23 +++- .../Dom/Internal/CssStyleSheet.cs | 19 ++- .../Dom/Internal/ICssDeclarationBuilder.cs | 12 ++ src/AngleSharp.Css/Dom/Internal/MediaList.cs | 19 ++- .../Dom/Internal/Rules/CssCharsetRule.cs | 4 +- .../Dom/Internal/Rules/CssContainerRule.cs | 9 +- .../Dom/Internal/Rules/CssDeclarationRule.cs | 15 ++- .../Dom/Internal/Rules/CssDescriptorRule.cs | 20 +++- .../Dom/Internal/Rules/CssDocumentRule.cs | 15 ++- .../Dom/Internal/Rules/CssGroupingRule.cs | 4 +- .../Dom/Internal/Rules/CssImportRule.cs | 2 +- .../Dom/Internal/Rules/CssKeyframeRule.cs | 4 +- .../Dom/Internal/Rules/CssKeyframesRule.cs | 5 +- .../Dom/Internal/Rules/CssNamespaceRule.cs | 16 ++- .../Dom/Internal/Rules/CssPageRule.cs | 4 +- .../Dom/Internal/Rules/CssScopeRule.cs | 4 +- .../Dom/Internal/Rules/CssStyleRule.cs | 7 +- .../Dom/Internal/Rules/CssSupportsRule.cs | 9 +- .../Extensions/StyleSheetExtensions.cs | 3 +- src/AngleSharp.Css/Parser/CssBuilder.cs | 18 +-- src/AngleSharp.Css/Parser/CssParser.cs | 1 - 26 files changed, 299 insertions(+), 61 deletions(-) create mode 100644 src/AngleSharp.Css/Dom/Internal/ICssDeclarationBuilder.cs diff --git a/docs/general/MutationVersion.md b/docs/general/MutationVersion.md index 8467784b..170cfac6 100644 --- a/docs/general/MutationVersion.md +++ b/docs/general/MutationVersion.md @@ -2,6 +2,14 @@ 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 diff --git a/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs b/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs index 777d7fd3..8afb8ff6 100644 --- a/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs +++ b/src/AngleSharp.Css.Tests/Rules/CssMutationVersion.cs @@ -1,10 +1,17 @@ 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; @@ -46,7 +53,7 @@ public void RuleMutationAdvancesVersion(String css, Action change) { var sheet = ParseStyleSheet(css); var before = sheet.GetMutationVersion(); - Assert.IsNotNull(before); + Assert.AreEqual(0, before, "Constructing a stylesheet must not advance its version."); change(sheet.Rules[0]); @@ -56,6 +63,87 @@ public void RuleMutationAdvancesVersion(String css, Action change) 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() { @@ -91,6 +179,25 @@ public void RevisionAdvancesBeforeDeclarationCallbacks() 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() { @@ -126,6 +233,7 @@ 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; 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 2cbcc46a..30d306a3 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs @@ -62,16 +62,26 @@ public String Value if (parsed != null && source.IsDone && parsed is not CssVarValue) { _value = parsed; - MutationOwner?.MarkChanged(); return; } } _value = _converter.Convert(value); - MutationOwner?.MarkChanged(); } } + 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; @@ -91,7 +101,7 @@ public String Value public Boolean IsImportant { get => _important; - set { _important = value; MutationOwner?.MarkChanged(); } + set => _important = value; } public String CssText => this.ToCss(); diff --git a/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs b/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs index 83240a30..b2175856 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssRuleList.cs @@ -13,16 +13,14 @@ sealed class CssRuleList : ICssRuleList #region Fields private readonly List _rules; - private readonly ICssMutationTracker _owner; #endregion #region ctor - internal CssRuleList(ICssMutationTracker owner) + internal CssRuleList() { _rules = new List(); - _owner = owner; } #endregion @@ -76,7 +74,6 @@ public Int32 Length public void Clear() { _rules.Clear(); - _owner.MarkChanged(); } public void RemoveAt(Int32 index) @@ -101,7 +98,6 @@ public void Remove(ICssRule rule) if (index >= 0) { _rules.RemoveAt(index); - _owner.MarkChanged(); } } } @@ -125,12 +121,10 @@ public void Insert(Int32 index, ICssRule rule) if (actualIndex == _rules.Count) { _rules.Add(rule); - _owner.MarkChanged(); } else { _rules.Insert(actualIndex, rule); - _owner.MarkChanged(); } } @@ -139,7 +133,6 @@ public void Add(ICssRule rule) if (rule != null) { _rules.Add(rule); - _owner.MarkChanged(); } } @@ -152,7 +145,6 @@ public void AddRange(IEnumerable rules) var oldLength = _rules.Count; _rules.AddRange(rules); - _owner.MarkChanged(); } internal IEnumerable GetFormattables() => _rules; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index 56514e9e..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, ICssMutationTracker + sealed class CssStyleDeclaration : ICssStyleDeclaration, ICssMutationTracker, ICssDeclarationBuilder { #region Fields @@ -290,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); @@ -313,15 +324,18 @@ 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) @@ -592,9 +606,10 @@ private void Track(ICssProperty property) private void RaiseChanged() { + MarkChanged(); + if (!_updating) { - MarkChanged(); _updating = true; Changed?.Invoke(CssText); _updating = false; diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs index d0162c58..48eda23a 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleSheet.cs @@ -35,7 +35,13 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) _context = context; _source = source; _media = new MediaList(context, this); - _rules = new CssRuleList(this); + _rules = new CssRuleList(); + } + + internal CssStyleSheet(IBrowsingContext context, TextSource source, Boolean disabled) + : this(context, source) + { + _disabled = disabled; } #endregion @@ -57,7 +63,14 @@ internal CssStyleSheet(IBrowsingContext context, TextSource source) public Boolean IsDisabled { get => _disabled; - set { _disabled = value; MarkChanged(); } + set + { + if (_disabled != value) + { + _disabled = value; + MarkChanged(); + } + } } internal Int64 MutationVersion => _mutationVersion; @@ -94,6 +107,7 @@ public void Add(ICssRule rule) public void Remove(ICssRule rule) { _rules.Remove(rule); + MarkChanged(); rule.SetOwner(null); } @@ -111,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/MediaList.cs b/src/AngleSharp.Css/Dom/Internal/MediaList.cs index 83dce21e..53b3752f 100644 --- a/src/AngleSharp.Css/Dom/Internal/MediaList.cs +++ b/src/AngleSharp.Css/Dom/Internal/MediaList.cs @@ -52,7 +52,18 @@ internal MediaList(IBrowsingContext context, ICssMutationTracker owner = null) 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 @@ -62,7 +73,11 @@ public String MediaText public void SetMediaText(String value, Boolean throwOnError) { _media.Clear(); - _owner?.MarkChanged(); + 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); diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssCharsetRule.cs index bb94381f..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; MarkChanged(); } + 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 36561b62..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; @@ -73,7 +79,6 @@ public Boolean SetConditionText(String value, Boolean throwOnError) _containerName = name; _containerQuery = query; - MarkChanged(); return true; } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs index f7143e44..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 @@ -84,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 @@ -112,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)) { @@ -136,8 +145,6 @@ protected void SetValue(String propertyName, String valueText) css.MutationOwner = this; } - MarkChanged(); - 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 4d8f75ab..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)) { @@ -68,7 +77,6 @@ public void SetProperty(String propertyName, String propertyValue, String priori if (property is not null) { _descriptors.Remove(property); - MarkChanged(); } return; @@ -144,9 +152,13 @@ public DescriptorProperty(String name, CssRule owner) public ICssValue RawValue => null; - public String Value { get => _value; set { _value = value; _owner.MarkChanged(); } } + 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 => _important; set { _important = value; _owner.MarkChanged(); } } + 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 a10b0c6d..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; @@ -48,7 +59,6 @@ protected override void ReplaceWith(ICssRule rule) base.ReplaceWith(rule); var newRule = (ICssDocumentRule)rule; _conditions.Clear(); - MarkChanged(); _conditions.AddRange(newRule.Conditions); } @@ -57,7 +67,6 @@ public Boolean SetConditionText(String value, Boolean throwOnError) var factory = Owner.Context.GetService(); var conditions = DocumentFunctionParser.Parse(value, factory); _conditions.Clear(); - MarkChanged(); if (conditions != null) { diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs index 8091c04f..c71c5a6f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssGroupingRule.cs @@ -20,7 +20,7 @@ abstract class CssGroupingRule : CssRule, ICssGroupingRule internal CssGroupingRule(ICssStyleSheet owner, CssRuleType type) : base(owner, type) { - _rules = new CssRuleList(this); + _rules = new CssRuleList(); } #endregion @@ -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 5569ab63..57d39a2f 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssImportRule.cs @@ -44,7 +44,7 @@ public String Href public ICssStyleSheet Sheet { get => _styleSheet; - set { _styleSheet = value; _styleSheet?.SetParent(Owner); MarkChanged(); } + set { _styleSheet = value; _styleSheet?.SetParent(Owner); } } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssKeyframeRule.cs index e1afc007..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); MarkChanged(); } + 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 9cf09019..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; MarkChanged(); } + 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/CssNamespaceRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssNamespaceRule.cs index 4087a822..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; MarkChanged(); } + set { InitializeNamespaceUri(value); MarkChanged(); } } public String Prefix { get => _prefix; - set { CheckValidity(); _prefix = value ?? String.Empty; MarkChanged(); } + 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 b4ebe78b..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); MarkChanged(); } + 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 7100f5ed..1a966776 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssScopeRule.cs @@ -21,9 +21,11 @@ internal CssScopeRule(ICssStyleSheet owner) public String ScopeText { get => _scopeText; - set { _scopeText = value; MarkChanged(); } + set { InitializeScopeText(value); MarkChanged(); } } + internal void InitializeScopeText(String value) => _scopeText = value; + protected override void ReplaceWith(ICssRule rule) { base.ReplaceWith(rule); diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs index 4aac11d4..b74c5f75 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs @@ -32,7 +32,7 @@ internal CssStyleRule(ICssStyleSheet owner) : base(owner, CssRuleType.Style) { _style = new CssStyleDeclaration(this); - _rules = new CssRuleList(this); + _rules = new CssRuleList(); _selectorList = null; } @@ -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,11 +154,12 @@ 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; _selector = value; - MarkChanged(); value?.Accept(this); } diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssSupportsRule.cs index 3521d8b3..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; @@ -56,7 +62,6 @@ public Boolean SetConditionText(String value, Boolean throwOnError) } _condition = condition; - MarkChanged(); return true; } diff --git a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs index 04cd919a..2fdd0020 100644 --- a/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs +++ b/src/AngleSharp.Css/Extensions/StyleSheetExtensions.cs @@ -19,7 +19,8 @@ public static class StyleSheetExtensions /// An opaque version, or null for an unsupported implementation. /// /// Compare readings for equality, on the owning thread. No-op writes may advance the - /// version. Track imported sheets separately, along with document and render-device state. + /// 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) => 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);