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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# 1.2.1

Released on Friday, October 23 2026.

- Fixed computed `display` not blockified for floats
- Fixed computed `display` not blockified for absolutely positioned boxes
- FIxed computed `display` not blockified for the root element
- Added support for `::placeholder`

# 1.2.0

Released on Wednesday, October 21 2026.
Expand Down
2 changes: 1 addition & 1 deletion src/AngleSharp.Css.Docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/css",
"version": "1.2.0",
"version": "1.2.1",
"preview": true,
"description": "The doclet for the AngleSharp.Css documentation.",
"keywords": [
Expand Down
176 changes: 176 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/BlockificationAndPlaceholderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Css.Dom;
using AngleSharp.Css.Parser;
using AngleSharp.Css.RenderTree;
using AngleSharp.Dom;
using NUnit.Framework;
using System.Linq;
using System.Threading.Tasks;

/// <summary>
/// The computed `display` of a floated box, an absolutely positioned box and the root element
/// is blockified (CSS Display 3 §2.7), and `::placeholder` (CSS Pseudo-Elements 4 §4.3) is
/// styled from CSS. Found in a downstream renderer (AngleSharp.Renderer): a floated inline
/// computed as `inline` and caused unbounded recursion, and placeholder text could not be
/// styled from CSS.
/// </summary>
[TestFixture]
public class BlockificationAndPlaceholderTests
{
private static async Task<IDocument> LoadAsync(string html)
{
var context = BrowsingContext.New(Configuration.Default.WithCss());
return await context.OpenAsync(req => req.Content(html));
}

[TestCase("float: left", "inline", "block")]
[TestCase("float: right", "inline-block", "block")]
[TestCase("float: left", "inline-flex", "flex")]
[TestCase("float: left", "inline-grid", "grid")]
[TestCase("float: left", "inline-table", "table")]
[TestCase("float: left", "inline list-item", "list-item")]
[TestCase("float: left", "table-cell", "block")]
[TestCase("float: left", "ruby", "block ruby")]
[TestCase("position: absolute", "inline", "block")]
[TestCase("position: fixed", "inline-flex", "flex")]
[TestCase("float: left", "none", "none")]
[TestCase("float: left", "contents", "contents")]
[TestCase("float: left", "flow-root", "flow-root")]
[TestCase("float: none", "inline", "inline")]
[TestCase("position: relative", "inline", "inline")]
public async Task FloatsAndAbsolutelyPositionedBoxesAreBlockified(string declaration, string display, string expected)
{
var document = await LoadAsync($"<span id=s style='{declaration}; display: {display}'>A</span>");
var element = document.GetElementById("s");

var computed = document.DefaultView.GetComputedStyle(element);

Assert.AreEqual(expected, computed.GetDisplay());
}

[Test]
public async Task UnsetDisplayOfAFloatIsBlockified()
{
var document = await LoadAsync("<span id=s style='float: left'>A</span>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("s"));

Assert.AreEqual("block", computed.GetDisplay());
}

[Test]
public async Task FloatedPseudoElementIsBlockified()
{
var document = await LoadAsync("<style>#s::before { content: 'x'; float: left }</style><span id=s>A</span>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("s"), "::before");

Assert.AreEqual("block", computed.GetDisplay());
}

[Test]
public async Task RootElementIsBlockified()
{
var document = await LoadAsync("<style>html { display: inline }</style>");

var computed = document.DefaultView.GetComputedStyle(document.DocumentElement);

Assert.AreEqual("block", computed.GetDisplay());
}

[Test]
public async Task RenderTreeCarriesTheBlockifiedDisplay()
{
var document = await LoadAsync("<span id=s style='float: left'>A</span>");

var tree = document.DefaultView.Render();
var node = Find(tree, document.GetElementById("s"));

Assert.IsNotNull(node);
Assert.AreEqual("block", node.ComputedStyle.GetDisplay());
}

[Test]
public async Task PlaceholderPseudoElementTakesAuthorRules()
{
var document = await LoadAsync("<style>#b::placeholder { color: rgb(1, 2, 3) }</style><input id=b placeholder=x>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("b"), "::placeholder");

Assert.AreEqual("rgba(1, 2, 3, 1)", computed.GetColor());
}

[Test]
public async Task PlaceholderPseudoElementInheritsFromItsElement()
{
var document = await LoadAsync("<style>#b { font-size: 30px }</style><input id=b placeholder=x>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("b"), "::placeholder");

Assert.AreEqual("30px", computed.GetFontSize());
}

[Test]
public async Task PlaceholderPseudoElementHasADefaultLook()
{
var document = await LoadAsync("<input id=b placeholder=x>");

var placeholder = document.DefaultView.GetComputedStyle(document.GetElementById("b"), "::placeholder");
var element = document.DefaultView.GetComputedStyle(document.GetElementById("b"));

Assert.AreNotEqual(element.GetColor(), placeholder.GetColor());
}

[Test]
public async Task PlaceholderPseudoElementOfATextareaTakesAuthorRules()
{
var document = await LoadAsync("<style>textarea::placeholder { color: rgb(1, 2, 3) }</style><textarea id=b placeholder=x></textarea>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("b"), "::placeholder");

Assert.AreEqual("rgba(1, 2, 3, 1)", computed.GetColor());
}

[Test]
public async Task PlaceholderRulesDoNotApplyToTheElementItself()
{
var document = await LoadAsync("<style>#b::placeholder { color: rgb(1, 2, 3) }</style><input id=b placeholder=x>");

var computed = document.DefaultView.GetComputedStyle(document.GetElementById("b"));

Assert.AreNotEqual("rgba(1, 2, 3, 1)", computed.GetColor());
}

[Test]
public async Task PlaceholderPseudoElementDoesNotExistWithoutAPlaceholder()
{
var document = await LoadAsync("<input id=b><div id=d placeholder=x></div>");

Assert.IsNull(document.GetElementById("b").Pseudo("placeholder"));
Assert.IsNull(document.GetElementById("d").Pseudo("placeholder"));
}

[Test]
public void PlaceholderSelectorParses()
{
var parser = new CssParser();

var sheet = parser.ParseStyleSheet("input::placeholder { color: red }");

Assert.AreEqual(1, sheet.Rules.Length);
Assert.AreEqual("input::placeholder", ((ICssStyleRule)sheet.Rules[0]).SelectorText);
}

private static ElementRenderNode Find(IRenderNode node, INode target)
{
if (node.Ref == target)
{
return node as ElementRenderNode;
}

return node.Children.Select(child => Find(child, target)).FirstOrDefault(found => found is not null);
}
}
}
3 changes: 2 additions & 1 deletion src/AngleSharp.Css.Tests/Styling/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public void ObtainDefaultSheet()
// and the block margins / list padding.
// 72, not 70: the UA stylesheet now also generates quotes for `q::before` and
// `q::after`.
Assert.AreEqual(72, sheet.Rules.Length);
// 73, not 72: the UA stylesheet now also gives `::placeholder` its default look.
Assert.AreEqual(73, sheet.Rules.Length);
}
}
}
9 changes: 9 additions & 0 deletions src/AngleSharp.Css/CssConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public static IConfiguration WithCss(this IConfiguration configuration, CssParse
configuration = configuration.WithOnly<IPseudoClassSelectorFactory>(new ForcingPseudoClassSelectorFactory(pseudoClassFactory));
}

// Wraps whatever pseudo-element selector factory is registered so that pseudo-elements
// only supported with CSS (e.g. ::placeholder) are recognized as well.
var pseudoElementFactory = configuration.Services.OfType<IPseudoElementSelectorFactory>().FirstOrDefault();

if (pseudoElementFactory is not null && pseudoElementFactory is not CssPseudoElementSelectorFactory)
{
configuration = configuration.WithOnly<IPseudoElementSelectorFactory>(new CssPseudoElementSelectorFactory(pseudoElementFactory));
}

return configuration
.WithOnly(Factory.Observer)
.WithOnly<IStylingService>(service);
Expand Down
1 change: 1 addition & 0 deletions src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ private static ICssStyleSheet Parse(String source)
input[type=range] { border: none; padding: 0px; background-color: transparent }
input[type=hidden] { display: none }
textarea { white-space: pre-wrap }
::placeholder { color: darkgray }
big { font-size: 1.17em }
small, sub, sup { font-size: .83em }
sub { vertical-align: sub }
Expand Down
27 changes: 27 additions & 0 deletions src/AngleSharp.Css/Dom/Internal/CssPseudoElementSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#nullable enable
namespace AngleSharp.Css.Dom
{
using AngleSharp.Dom;
using System;

/// <summary>
/// Matches the pseudo-element with the given name, e.g. `::placeholder`.
/// </summary>
sealed class CssPseudoElementSelector : ISelector
{
private readonly String _name;

public CssPseudoElementSelector(String name)
{
_name = name;
}

public Priority Specificity => Priority.OneTag;

public String Text => PseudoElementNames.Separator + CssUtilities.Escape(_name);

public void Accept(ISelectorVisitor visitor) => visitor.PseudoElement(_name);

public Boolean Match(IElement element, IElement? scope) => element.IsPseudo(_name);
}
}
90 changes: 90 additions & 0 deletions src/AngleSharp.Css/Extensions/CssOmExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,95 @@ internal static CssStyleDeclaration PrepareComputedDeclarations(this ICssStyleDe
declarations.UpdateDeclarations(parent);
return declarations;
}

/// <summary>
/// Blockifies the computed display of a floated box, an absolutely positioned box
/// and the root element, see CSS Display 3 §2.7 and CSS 2.1 §9.7.
/// </summary>
/// <param name="style">The computed style of the element.</param>
/// <param name="element">The element the style was computed for.</param>
/// <returns>The computed style, adjusted if needed.</returns>
internal static ICssStyleDeclaration Blockify(this ICssStyleDeclaration style, IElement element)
{
if (style is CssStyleDeclaration declarations && IsBlockified(declarations, element))
{
var current = declarations.GetProperty(PropertyNames.Display);
var value = Blockify(current?.RawValue);

if (value is not null)
{
var info = element.Owner.Context.GetDeclarationInfo(PropertyNames.Display);

if (current is not null)
{
declarations.RemoveProperty(current);
}

declarations.AddProperty(new CssProperty(PropertyNames.Display, info.Converter, info.Flags, value, current?.IsImportant ?? false));
}
}

return style;
}

private static Boolean IsBlockified(CssStyleDeclaration style, IElement element)
{
if (element is not IPseudoElement && element.Owner?.DocumentElement == element)
{
return true;
}

var floating = style.GetPropertyValue(PropertyNames.Float);
var position = style.GetPropertyValue(PropertyNames.Position);
return (!String.IsNullOrEmpty(floating) && !floating.Isi(CssKeywords.None)) ||
position.Isi(CssKeywords.Absolute) || position.Isi(CssKeywords.Fixed);
}

private static ICssValue Blockify(ICssValue display)
{
// A missing display property computes to its initial value, i.e., inline.
if (display is null)
{
return CreateDisplay(CssKeywords.Block, DisplayMode.Block);
}
else if (display is CssConstantValue<DisplayMode> mode)
{
return Blockify(mode);
}

return null;
}

private static ICssValue Blockify(CssConstantValue<DisplayMode> mode)
{
switch (mode.Value)
{
case DisplayMode.Inline:
case DisplayMode.InlineBlock:
case DisplayMode.TableCaption:
case DisplayMode.TableCell:
case DisplayMode.TableColumn:
case DisplayMode.TableColumnGroup:
case DisplayMode.TableFooterGroup:
case DisplayMode.TableHeaderGroup:
case DisplayMode.TableRow:
case DisplayMode.TableRowGroup:
return CreateDisplay(CssKeywords.Block, DisplayMode.Block);
case DisplayMode.InlineTable:
return CreateDisplay(CssKeywords.Table, DisplayMode.Table);
case DisplayMode.InlineFlex:
return CreateDisplay(CssKeywords.Flex, DisplayMode.Flex);
case DisplayMode.InlineGrid:
return CreateDisplay(CssKeywords.Grid, DisplayMode.Grid);
case DisplayMode.ListItem when mode.CssText.StartsWith(CssKeywords.Inline + " ", StringComparison.OrdinalIgnoreCase):
return CreateDisplay(mode.CssText.Substring(CssKeywords.Inline.Length + 1), DisplayMode.ListItem);
case DisplayMode.Ruby when mode.CssText.Isi(CssKeywords.Ruby):
return CreateDisplay(String.Concat(CssKeywords.Block, " ", CssKeywords.Ruby), DisplayMode.Ruby);
default:
return null;
}
}

private static ICssValue CreateDisplay(String keyword, DisplayMode mode) => new CssConstantValue<DisplayMode>(keyword, mode);
}
}
7 changes: 4 additions & 3 deletions src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ private static ICssStyleDeclaration GetComputedDeclarations(IStyleCollection sty

while (nodes.Count > 0)
{
var explicitStyle = styles.ComputeExplicitStyle(nodes.Pop());
var node = nodes.Pop();
var explicitStyle = styles.ComputeExplicitStyle(node);
var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parent);
parent = explicitStyle.PrepareComputedDeclarations(parent!, context).Compute(context);
parent = explicitStyle.PrepareComputedDeclarations(parent!, context).Compute(context).Blockify(node);
}

return parent!;
Expand Down Expand Up @@ -163,7 +164,7 @@ internal static ICssStyleDeclaration ComputeDeclarationsWithParent(this IStyleCo
var ctx = element.Owner?.Context;
var explicitStyle = styles.ComputeExplicitStyle(element);
var context = new CssComputeContext(styles.Device, ctx, explicitStyle, parentComputedStyle);
return explicitStyle.PrepareComputedDeclarations(parentComputedStyle, context).Compute(context);
return explicitStyle.PrepareComputedDeclarations(parentComputedStyle, context).Compute(context).Blockify(element);
}

#endregion
Expand Down
Loading
Loading