diff --git a/CHANGELOG.md b/CHANGELOG.md
index a600b32e..912c852b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json
index cbb453cb..05b6fdd6 100644
--- a/src/AngleSharp.Css.Docs/package.json
+++ b/src/AngleSharp.Css.Docs/package.json
@@ -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": [
diff --git a/src/AngleSharp.Css.Tests/Styling/BlockificationAndPlaceholderTests.cs b/src/AngleSharp.Css.Tests/Styling/BlockificationAndPlaceholderTests.cs
new file mode 100644
index 00000000..623f2b5c
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/BlockificationAndPlaceholderTests.cs
@@ -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;
+
+ ///
+ /// 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.
+ ///
+ [TestFixture]
+ public class BlockificationAndPlaceholderTests
+ {
+ private static async Task 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($"A");
+ 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("A");
+
+ var computed = document.DefaultView.GetComputedStyle(document.GetElementById("s"));
+
+ Assert.AreEqual("block", computed.GetDisplay());
+ }
+
+ [Test]
+ public async Task FloatedPseudoElementIsBlockified()
+ {
+ var document = await LoadAsync("A");
+
+ var computed = document.DefaultView.GetComputedStyle(document.GetElementById("s"), "::before");
+
+ Assert.AreEqual("block", computed.GetDisplay());
+ }
+
+ [Test]
+ public async Task RootElementIsBlockified()
+ {
+ var document = await LoadAsync("");
+
+ var computed = document.DefaultView.GetComputedStyle(document.DocumentElement);
+
+ Assert.AreEqual("block", computed.GetDisplay());
+ }
+
+ [Test]
+ public async Task RenderTreeCarriesTheBlockifiedDisplay()
+ {
+ var document = await LoadAsync("A");
+
+ 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("");
+
+ 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("");
+
+ var computed = document.DefaultView.GetComputedStyle(document.GetElementById("b"), "::placeholder");
+
+ Assert.AreEqual("30px", computed.GetFontSize());
+ }
+
+ [Test]
+ public async Task PlaceholderPseudoElementHasADefaultLook()
+ {
+ var document = await LoadAsync("");
+
+ 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("");
+
+ 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("");
+
+ 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("");
+
+ 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);
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/Configuration.cs b/src/AngleSharp.Css.Tests/Styling/Configuration.cs
index 949238cb..75d297d2 100644
--- a/src/AngleSharp.Css.Tests/Styling/Configuration.cs
+++ b/src/AngleSharp.Css.Tests/Styling/Configuration.cs
@@ -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);
}
}
}
diff --git a/src/AngleSharp.Css/CssConfigurationExtensions.cs b/src/AngleSharp.Css/CssConfigurationExtensions.cs
index da30661d..d6001743 100644
--- a/src/AngleSharp.Css/CssConfigurationExtensions.cs
+++ b/src/AngleSharp.Css/CssConfigurationExtensions.cs
@@ -63,6 +63,15 @@ public static IConfiguration WithCss(this IConfiguration configuration, CssParse
configuration = configuration.WithOnly(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().FirstOrDefault();
+
+ if (pseudoElementFactory is not null && pseudoElementFactory is not CssPseudoElementSelectorFactory)
+ {
+ configuration = configuration.WithOnly(new CssPseudoElementSelectorFactory(pseudoElementFactory));
+ }
+
return configuration
.WithOnly(Factory.Observer)
.WithOnly(service);
diff --git a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs
index 21609244..e45c0cd1 100644
--- a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs
+++ b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs
@@ -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 }
diff --git a/src/AngleSharp.Css/Dom/Internal/CssPseudoElementSelector.cs b/src/AngleSharp.Css/Dom/Internal/CssPseudoElementSelector.cs
new file mode 100644
index 00000000..061af48c
--- /dev/null
+++ b/src/AngleSharp.Css/Dom/Internal/CssPseudoElementSelector.cs
@@ -0,0 +1,27 @@
+#nullable enable
+namespace AngleSharp.Css.Dom
+{
+ using AngleSharp.Dom;
+ using System;
+
+ ///
+ /// Matches the pseudo-element with the given name, e.g. `::placeholder`.
+ ///
+ 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);
+ }
+}
diff --git a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
index 3ccbc7a1..13815279 100644
--- a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs
@@ -162,5 +162,95 @@ internal static CssStyleDeclaration PrepareComputedDeclarations(this ICssStyleDe
declarations.UpdateDeclarations(parent);
return declarations;
}
+
+ ///
+ /// 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.
+ ///
+ /// The computed style of the element.
+ /// The element the style was computed for.
+ /// The computed style, adjusted if needed.
+ 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 mode)
+ {
+ return Blockify(mode);
+ }
+
+ return null;
+ }
+
+ private static ICssValue Blockify(CssConstantValue 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(keyword, mode);
}
}
diff --git a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
index 2660be07..90b4c4d7 100644
--- a/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
+++ b/src/AngleSharp.Css/Extensions/StyleCollectionExtensions.cs
@@ -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!;
@@ -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
diff --git a/src/AngleSharp.Css/Factories/CssPseudoElementSelectorFactory.cs b/src/AngleSharp.Css/Factories/CssPseudoElementSelectorFactory.cs
new file mode 100644
index 00000000..b96e990a
--- /dev/null
+++ b/src/AngleSharp.Css/Factories/CssPseudoElementSelectorFactory.cs
@@ -0,0 +1,39 @@
+#nullable disable
+namespace AngleSharp.Css
+{
+ using AngleSharp.Css.Dom;
+ using System;
+
+ ///
+ /// Decorates another pseudo-element selector factory with the pseudo-elements
+ /// that are supported by AngleSharp.Css, but not recognized by the decorated one.
+ ///
+ sealed class CssPseudoElementSelectorFactory : IPseudoElementSelectorFactory
+ {
+ ///
+ /// The name of the `::placeholder` pseudo-element (CSS Pseudo-Elements 4 §4.3).
+ ///
+ public static readonly String Placeholder = "placeholder";
+
+ private static readonly ISelector PlaceholderSelector = new CssPseudoElementSelector(Placeholder);
+
+ private readonly IPseudoElementSelectorFactory _inner;
+
+ public CssPseudoElementSelectorFactory(IPseudoElementSelectorFactory inner)
+ {
+ _inner = inner;
+ }
+
+ public ISelector Create(String name)
+ {
+ var selector = _inner.Create(name);
+
+ if (selector is null && name.Equals(Placeholder, StringComparison.OrdinalIgnoreCase))
+ {
+ return PlaceholderSelector;
+ }
+
+ return selector;
+ }
+ }
+}
diff --git a/src/AngleSharp.Css/Factories/DefaultPseudoElementFactory.cs b/src/AngleSharp.Css/Factories/DefaultPseudoElementFactory.cs
index 54a760b0..93d63253 100644
--- a/src/AngleSharp.Css/Factories/DefaultPseudoElementFactory.cs
+++ b/src/AngleSharp.Css/Factories/DefaultPseudoElementFactory.cs
@@ -3,6 +3,7 @@ namespace AngleSharp.Css
{
using AngleSharp.Css.Dom;
using AngleSharp.Dom;
+ using AngleSharp.Html.Dom;
using System;
using System.Collections.Generic;
@@ -23,8 +24,20 @@ public class DefaultPseudoElementFactory : IPseudoElementFactory
{ PseudoElementNames.Before, element => new PseudoElement(element, PseudoElementNames.Before) },
{ PseudoElementNames.After, element => new PseudoElement(element, PseudoElementNames.After) },
{ PseudoElementNames.Slotted, element => new PseudoElement(element, PseudoElementNames.Slotted) },
+ { CssPseudoElementSelectorFactory.Placeholder, CreatePlaceholder },
};
+ private static IPseudoElement CreatePlaceholder(IElement host)
+ {
+ // Only an input or a textarea with placeholder text has a placeholder.
+ if ((host is IHtmlInputElement || host is IHtmlTextAreaElement) && host.HasAttribute(AttributeNames.Placeholder))
+ {
+ return new PseudoElement(host, CssPseudoElementSelectorFactory.Placeholder);
+ }
+
+ return null;
+ }
+
///
/// Registers a new creator for the specified pseudo type.
/// Throws an exception if another creator for the given
diff --git a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
index f42f1540..3dc307ff 100644
--- a/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
+++ b/src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
@@ -93,7 +93,7 @@ private ElementRenderNode RenderElement(
}
var computeContext = new CssComputeContext(collection.Device, _context, explicitStyle, parentComputedStyle);
- var computedStyle = explicitStyle.PrepareComputedDeclarations(parentComputedStyle!, computeContext).Compute(computeContext);
+ var computedStyle = explicitStyle.PrepareComputedDeclarations(parentComputedStyle!, computeContext).Compute(computeContext).Blockify(element);
var children = new List();
var node = new ElementRenderNode(element, parent, children, specifiedStyle, computedStyle);
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 627d54f6..c4e1a6fd 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,7 +2,7 @@
Extends the CSSOM from the core AngleSharp library.
AngleSharp.Css
- 1.2.0
+ 1.2.1
enable
latest
true