Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ private ICssProperty TryCreateShorthand(String shorthandName, IEnumerable<String

if (requiredProperties.Length > 0)
{
var longhands = Declarations.Where(m => !serialized.Contains(m.Name)).ToList();
var values = new ICssValue[requiredProperties.Length];
var important = 0;
var count = 0;
Expand All @@ -135,9 +134,9 @@ private ICssProperty TryCreateShorthand(String shorthandName, IEnumerable<String
{
var name = requiredProperties[i];
var propInfo = factory.Create(name);
var property = propInfo.Longhands.Any() ?
var property = propInfo.Longhands.Length > 0 ?
TryCreateShorthand(name, serialized, usedProperties, force) :
longhands.Where(m => m.Name == name).FirstOrDefault();
FindUnserializedLonghand(name, serialized);

if (property?.Value is not null)
{
Expand Down Expand Up @@ -340,6 +339,39 @@ internal void UpdateDeclarations(IEnumerable<ICssProperty> decls) =>
private ICssProperty GetPropertyShorthand(String name) =>
TryCreateShorthand(name, Enumerable.Empty<String>(), new List<String>(), true);

/// <summary>
/// Gets the first declaration with the given name, unless that name was
/// already serialized. Equivalent to filtering all declarations by the
/// serialized set first, since only declarations with exactly this name
/// can ever be returned.
/// </summary>
private ICssProperty FindUnserializedLonghand(String name, IEnumerable<String> serialized)
{
if (serialized is ICollection<String> collection)
{
if (collection.Count > 0 && collection.Contains(name))
{
return null;
}
}
else if (serialized.Contains(name))
{
return null;
}

for (var i = 0; i < _declarations.Count; i++)
{
var declaration = _declarations[i];

if (declaration.Name == name)
{
return declaration;
}
}

return null;
}

private ICssProperty CreateProperty(String propertyName)
{
var newProperty = _context.CreateProperty(propertyName);
Expand Down
13 changes: 10 additions & 3 deletions src/AngleSharp.Css/Dom/Internal/Rules/CssStyleRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ sealed class CssStyleRule : CssRule, ICssStyleRule, ISelectorVisitor
private readonly CssStyleDeclaration _style;
private readonly CssRuleList _rules;
private ISelector _selector;
private IEnumerable<ISelector> _selectorList;
private ISelector[] _selectorList;
private Boolean _nested;

#endregion
Expand Down Expand Up @@ -120,8 +120,13 @@ public Boolean TryMatch(IElement element, IElement? scope, out Priority specific

if (_selectorList is not null)
{
foreach (var selector in _selectorList.OrderByDescending(m => m.Specificity))
// Already ordered by descending specificity when the selector was
// assigned - sorting here would repeat the work for every single
// element the rule is matched against.
for (var i = 0; i < _selectorList.Length; i++)
{
var selector = _selectorList[i];

if (selector.Match(element, scope))
{
specificity += selector.Specificity;
Expand Down Expand Up @@ -186,7 +191,9 @@ void ISelectorVisitor.PseudoElement(string name)

void ISelectorVisitor.List(IEnumerable<ISelector> selectors)
{
_selectorList = selectors;
// OrderByDescending is stable, so selectors of equal specificity keep
// their declared order - same as when this ran per match attempt.
_selectorList = selectors.OrderByDescending(m => m.Specificity).ToArray();
}

void ISelectorVisitor.Combinator(IEnumerable<ISelector> selectors, IEnumerable<string> symbols)
Expand Down
34 changes: 25 additions & 9 deletions src/AngleSharp.Css/Dom/Internal/StyleCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ namespace AngleSharp.Css.Dom
using AngleSharp.Dom;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

sealed class StyleCollection : IStyleCollection
{
#region Fields

private readonly IEnumerable<ICssStyleSheet> _sheets;
private readonly IRenderDevice _device;
private List<ICssStyleRule>? _rules;

#endregion

Expand All @@ -31,25 +33,39 @@ public StyleCollection(IEnumerable<ICssStyleSheet> sheets, IRenderDevice device)

#region Methods

public IEnumerator<ICssStyleRule> GetEnumerator()
public IEnumerator<ICssStyleRule> GetEnumerator() => GetRules().GetEnumerator();

/// <summary>
/// Gets the flattened list of style rules that apply to the current
/// device. The supplied sheet sequence is usually a lazy query over the
/// document, so it is walked exactly once and the result is reused for
/// the lifetime of the collection - which spans a single cascade or
/// render pass. Without this the whole DOM would be re-walked for every
/// element, and for every one of its ancestors.
/// </summary>
internal List<ICssStyleRule> GetRules() => _rules ??= CollectRules();

#endregion

#region Helpers

private List<ICssStyleRule> CollectRules()
{
var rules = new List<ICssStyleRule>();

foreach (var sheet in _sheets)
{
if (!sheet.IsDisabled && sheet.Media.Validate(_device))
{
var rules = sheet.Rules.GetMatchingStyles(_device);

foreach (var rule in rules)
foreach (var rule in sheet.Rules.GetMatchingStyles(_device))
{
yield return rule;
rules.Add(rule);
}
}
}
}

#endregion

#region Helpers
return rules;
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

Expand Down
83 changes: 65 additions & 18 deletions src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ public static ICssStyleDeclaration ComputeExplicitStyle(this IStyleCollection st
{
var ctx = element.Owner?.Context ?? throw new InvalidOperationException("The element must be associated with a browsing context.");
var computedStyle = new CssStyleDeclaration(ctx);
var rules = styles.SortBySpecificity(element);
var matches = styles.SortBySpecificity(element);

foreach (var rule in rules)
for (var i = 0; i < matches.Count; i++)
{
var inlineStyle = rule.Style;
var inlineStyle = matches[i].Rule.Style;
computedStyle.SetDeclarations(inlineStyle);
}

Expand Down Expand Up @@ -158,33 +158,80 @@ internal static ICssStyleDeclaration ComputeDeclarationsWithParent(this IStyleCo

#region Helpers

private static IEnumerable<ICssStyleRule> SortBySpecificity(this IEnumerable<ICssStyleRule> rules, IElement element)
private static List<RuleMatch> SortBySpecificity(this IStyleCollection styles, IElement element)
{
IEnumerable<Tuple<ICssStyleRule, Priority>> MapPriority(ICssStyleRule rule)
// Resolving the scope once is equivalent to letting every TryMatch call
// fall back to it, but avoids re-reading DocumentElement per rule.
var scope = element.Owner?.DocumentElement;
var matches = new List<RuleMatch>();

if (styles is StyleCollection collection)
{
if (rule.TryMatch(element, null, out var specificity))
var rules = collection.GetRules();

for (var i = 0; i < rules.Count; i++)
{
yield return Tuple.Create(rule, specificity);
MapPriority(rules[i], element, scope, matches);
}
}
else
{
foreach (var rule in styles)
{
MapPriority(rule, element, scope, matches);
}
}

// OrderBy is a stable sort; the index tie-break reproduces that for
// rules that share the same specificity.
matches.Sort(RuleMatchComparer.Instance);
return matches;
}

private static void MapPriority(ICssStyleRule rule, IElement element, IElement? scope, List<RuleMatch> matches)
{
if (rule.TryMatch(element, scope, out var specificity))
{
matches.Add(new RuleMatch(rule, specificity, matches.Count));
}

foreach (var subRule in rule.Rules)
var subRules = rule.Rules;

for (var i = 0; i < subRules.Length; i++)
{
if (subRules[i] is ICssStyleRule style)
{
if (subRule is ICssStyleRule style)
{
foreach (var item in MapPriority(style))
{
yield return item;
}
}
MapPriority(style, element, scope, matches);
}
}
}

private readonly struct RuleMatch
{
public RuleMatch(ICssStyleRule rule, Priority priority, Int32 index)
{
Rule = rule;
Priority = priority;
Index = index;
}

return rules.SelectMany(MapPriority).OrderBy(GetPriority).Select(GetRule);
public ICssStyleRule Rule { get; }

public Priority Priority { get; }

public Int32 Index { get; }
}

private static Priority GetPriority(Tuple<ICssStyleRule, Priority> item) => item.Item2;
private sealed class RuleMatchComparer : IComparer<RuleMatch>
{
public static readonly RuleMatchComparer Instance = new RuleMatchComparer();

private static ICssStyleRule GetRule(Tuple<ICssStyleRule, Priority> item) => item.Item1;
public Int32 Compare(RuleMatch x, RuleMatch y)
{
var result = Comparer<Priority>.Default.Compare(x.Priority, y.Priority);
return result != 0 ? result : x.Index.CompareTo(y.Index);
}
}

#endregion
}
Expand Down
Loading
Loading