diff --git a/src/AngleSharp.Css/Extensions/ElementExtensions.cs b/src/AngleSharp.Css/Extensions/ElementExtensions.cs index cc25785..90900e6 100644 --- a/src/AngleSharp.Css/Extensions/ElementExtensions.cs +++ b/src/AngleSharp.Css/Extensions/ElementExtensions.cs @@ -47,9 +47,17 @@ public static String GetInnerText(this IElement element) hidden = true; } + // Build the style collection once and cache computed styles for the + // entire traversal to avoid redundant stylesheet enumeration and + // repeated selector matching for the same elements. + var document = element.Owner; + var window = document?.DefaultView; + var styleCollection = window != null ? window.GetStyleCollection() : null; + var styleCache = new Dictionary(); + if (!hidden.HasValue) { - var css = element.ComputeCurrentStyle(); + var css = ComputeCachedStyle(element, styleCollection, styleCache); if (!String.IsNullOrEmpty(css?.GetDisplay())) { @@ -67,7 +75,10 @@ public static String GetInnerText(this IElement element) var offset = 0; var sb = StringBuilderPool.Obtain(); var requiredLineBreakCounts = new Dictionary(); - InnerTextCollection(element, sb, requiredLineBreakCounts, element.ParentElement?.ComputeCurrentStyle()); + var parentStyle = element.ParentElement != null + ? ComputeCachedStyle(element.ParentElement, styleCollection, styleCache) + : null; + InnerTextCollection(element, sb, requiredLineBreakCounts, parentStyle, styleCollection, styleCache); // Remove any runs of consecutive required line break count items at the start or end of results. requiredLineBreakCounts.Remove(0); @@ -87,6 +98,36 @@ public static String GetInnerText(this IElement element) return element.TextContent; } + private static ICssStyleDeclaration ComputeCachedStyle(IElement element, IStyleCollection styleCollection, Dictionary cache) + { + if (styleCollection == null) + { + return null; + } + + if (cache.TryGetValue(element, out var cached)) + { + return cached; + } + + ICssStyleDeclaration style; + var parent = element.ParentElement; + + if (parent != null && cache.TryGetValue(parent, out var parentStyle)) + { + // Parent already computed — use incremental computation that + // skips the O(depth) ancestor walk. + style = styleCollection.ComputeDeclarationsWithParent(element, parentStyle); + } + else + { + style = styleCollection.ComputeDeclarations(element); + } + + cache[element] = style; + return style; + } + /// /// Sets the innerText of an element. /// @@ -149,17 +190,17 @@ public static void SetInnerText(this IElement element, String value) } } - private static void InnerTextCollection(INode node, StringBuilder sb, Dictionary requiredLineBreakCounts, ICssStyleDeclaration parentStyle) + private static void InnerTextCollection(INode node, StringBuilder sb, Dictionary requiredLineBreakCounts, ICssStyleDeclaration parentStyle, IStyleCollection styleCollection, Dictionary styleCache) { if (HasCssBox(node)) { var element = node as IElement; - var elementCss = element?.ComputeCurrentStyle(); - ItcInCssBox(elementCss, parentStyle, node, sb, requiredLineBreakCounts); + var elementCss = element != null ? ComputeCachedStyle(element, styleCollection, styleCache) : null; + ItcInCssBox(elementCss, parentStyle, node, sb, requiredLineBreakCounts, styleCollection, styleCache); } } - private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDeclaration parentStyle, INode node, StringBuilder sb, Dictionary requiredLineBreakCounts) + private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDeclaration parentStyle, INode node, StringBuilder sb, Dictionary requiredLineBreakCounts, IStyleCollection styleCollection, Dictionary styleCache) { var elementHidden = new Nullable(); @@ -188,7 +229,7 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl foreach (var child in node.ChildNodes) { - InnerTextCollection(child, sb, requiredLineBreakCounts, elementStyle); + InnerTextCollection(child, sb, requiredLineBreakCounts, elementStyle, styleCollection, styleCache); } if (node is IText textElement) @@ -206,7 +247,7 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl { if (node.NextSibling is IElement nextSibling) { - var nextSiblingCss = nextSibling.ComputeCurrentStyle(); + var nextSiblingCss = ComputeCachedStyle(nextSibling, styleCollection, styleCache); if (nextSibling is IHtmlTableCellElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableCell) { @@ -218,7 +259,7 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl { if (node.NextSibling is IElement nextSibling) { - var nextSiblingCss = nextSibling.ComputeCurrentStyle(); + var nextSiblingCss = ComputeCachedStyle(nextSibling, styleCollection, styleCache); if (nextSibling is IHtmlTableRowElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableRow) { diff --git a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs index 09e3362..e1fa106 100644 --- a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs +++ b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs @@ -118,6 +118,31 @@ public static ICssStyleDeclaration ComputeCascadedStyle(this IStyleCollection st return computedStyle; } + /// + /// Computes the declarations for the given element using a pre-computed + /// parent style for inheritance, avoiding the O(depth) ancestor walk. + /// + /// The styles to use. + /// The element that is questioned. + /// The parent's already-computed style. + /// The style declaration containing all the declarations. + internal static ICssStyleDeclaration ComputeDeclarationsWithParent(this IStyleCollection styles, IElement element, ICssStyleDeclaration parentComputedStyle) + { + var ctx = element.Owner?.Context; + var computedStyle = new CssStyleDeclaration(ctx); + + // Element's own cascaded style (CSS rule matching + inline style). + computedStyle.SetDeclarations(styles.ComputeCascadedStyle(element)); + + // Inherit from the parent's already-computed style instead of walking + // all ancestors individually. The parent style already includes the + // full ancestor inheritance chain. + computedStyle.UpdateDeclarations(parentComputedStyle); + + var context = new CssComputeContext(styles.Device, ctx, computedStyle); + return computedStyle.Compute(context); + } + #endregion #region Helpers