LibWeb: Parse anchor() function for inset properties

This commit is contained in:
Tim Ledbetter 2025-08-03 13:58:13 +01:00 committed by Jelle Raaijmakers
commit 1d9e4a6f62
Notes: github-actions[bot] 2025-08-03 20:10:50 +00:00
17 changed files with 2725 additions and 5 deletions

View file

@ -178,6 +178,7 @@ set(SOURCES
CSS/StyleSheet.cpp CSS/StyleSheet.cpp
CSS/StyleSheetIdentifier.cpp CSS/StyleSheetIdentifier.cpp
CSS/StyleSheetList.cpp CSS/StyleSheetList.cpp
CSS/StyleValues/AnchorStyleValue.cpp
CSS/StyleValues/AnchorSizeStyleValue.cpp CSS/StyleValues/AnchorSizeStyleValue.cpp
CSS/StyleValues/AngleStyleValue.cpp CSS/StyleValues/AngleStyleValue.cpp
CSS/StyleValues/BackgroundRepeatStyleValue.cpp CSS/StyleValues/BackgroundRepeatStyleValue.cpp

View file

@ -14,6 +14,7 @@
#include <LibWeb/CSS/Parser/Parser.h> #include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h> #include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
#include <LibWeb/CSS/StyleValues/AnchorSizeStyleValue.h> #include <LibWeb/CSS/StyleValues/AnchorSizeStyleValue.h>
#include <LibWeb/CSS/StyleValues/AnchorStyleValue.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h> #include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/BackgroundRepeatStyleValue.h> #include <LibWeb/CSS/StyleValues/BackgroundRepeatStyleValue.h>
#include <LibWeb/CSS/StyleValues/BackgroundSizeStyleValue.h> #include <LibWeb/CSS/StyleValues/BackgroundSizeStyleValue.h>
@ -84,6 +85,12 @@ AbstractImageStyleValue const& CSSStyleValue::as_abstract_image() const
return static_cast<AbstractImageStyleValue const&>(*this); return static_cast<AbstractImageStyleValue const&>(*this);
} }
AnchorStyleValue const& CSSStyleValue::as_anchor() const
{
VERIFY(is_anchor());
return static_cast<AnchorStyleValue const&>(*this);
}
AnchorSizeStyleValue const& CSSStyleValue::as_anchor_size() const AnchorSizeStyleValue const& CSSStyleValue::as_anchor_size() const
{ {
VERIFY(is_anchor_size()); VERIFY(is_anchor_size());

View file

@ -87,6 +87,7 @@ public:
virtual ~CSSStyleValue() = default; virtual ~CSSStyleValue() = default;
enum class Type { enum class Type {
Anchor,
AnchorSize, AnchorSize,
Angle, Angle,
BackgroundRepeat, BackgroundRepeat,
@ -155,6 +156,10 @@ public:
AbstractImageStyleValue const& as_abstract_image() const; AbstractImageStyleValue const& as_abstract_image() const;
AbstractImageStyleValue& as_abstract_image() { return const_cast<AbstractImageStyleValue&>(const_cast<CSSStyleValue const&>(*this).as_abstract_image()); } AbstractImageStyleValue& as_abstract_image() { return const_cast<AbstractImageStyleValue&>(const_cast<CSSStyleValue const&>(*this).as_abstract_image()); }
bool is_anchor() const { return type() == Type::Anchor; }
AnchorStyleValue const& as_anchor() const;
AnchorStyleValue& as_anchor() { return const_cast<AnchorStyleValue&>(const_cast<CSSStyleValue const&>(*this).as_anchor()); }
bool is_anchor_size() const { return type() == Type::AnchorSize; } bool is_anchor_size() const { return type() == Type::AnchorSize; }
AnchorSizeStyleValue const& as_anchor_size() const; AnchorSizeStyleValue const& as_anchor_size() const;
AnchorSizeStyleValue& as_anchor_size() { return const_cast<AnchorSizeStyleValue&>(const_cast<CSSStyleValue const&>(*this).as_anchor_size()); } AnchorSizeStyleValue& as_anchor_size() { return const_cast<AnchorSizeStyleValue&>(const_cast<CSSStyleValue const&>(*this).as_anchor_size()); }

View file

@ -40,6 +40,19 @@
"stretch", "stretch",
"unsafe" "unsafe"
], ],
"anchor-side": [
"inside",
"outside",
"top",
"left",
"right",
"bottom",
"start",
"end",
"self-start",
"self-end",
"center"
],
"anchor-size": [ "anchor-size": [
"block", "block",
"height", "height",

View file

@ -396,6 +396,7 @@ private:
RefPtr<StringStyleValue const> parse_opentype_tag_value(TokenStream<ComponentValue>&); RefPtr<StringStyleValue const> parse_opentype_tag_value(TokenStream<ComponentValue>&);
RefPtr<FontSourceStyleValue const> parse_font_source_value(TokenStream<ComponentValue>&); RefPtr<FontSourceStyleValue const> parse_font_source_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue const> parse_anchor(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue const> parse_anchor_size(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue const> parse_anchor_size(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue const> parse_angle_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue const> parse_angle_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue const> parse_angle_percentage_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue const> parse_angle_percentage_value(TokenStream<ComponentValue>&);

View file

@ -384,6 +384,9 @@ Optional<Parser::PropertyAndValue> Parser::parse_css_value_for_properties(Readon
if (auto parsed = parse_for_type(ValueType::Paint); parsed.has_value()) if (auto parsed = parse_for_type(ValueType::Paint); parsed.has_value())
return parsed.release_value(); return parsed.release_value();
if (auto parsed = parse_for_type(ValueType::Anchor); parsed.has_value())
return parsed.release_value();
return OptionalNone {}; return OptionalNone {};
} }

View file

@ -23,6 +23,7 @@
#include <LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h> #include <LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h>
#include <LibWeb/CSS/Parser/Parser.h> #include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/AnchorSizeStyleValue.h> #include <LibWeb/CSS/StyleValues/AnchorSizeStyleValue.h>
#include <LibWeb/CSS/StyleValues/AnchorStyleValue.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h> #include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/BackgroundRepeatStyleValue.h> #include <LibWeb/CSS/StyleValues/BackgroundRepeatStyleValue.h>
#include <LibWeb/CSS/StyleValues/BackgroundSizeStyleValue.h> #include <LibWeb/CSS/StyleValues/BackgroundSizeStyleValue.h>
@ -795,6 +796,75 @@ RefPtr<CSSStyleValue const> Parser::parse_percentage_value(TokenStream<Component
return nullptr; return nullptr;
} }
// https://drafts.csswg.org/css-anchor-position-1/#funcdef-anchor
RefPtr<CSSStyleValue const> Parser::parse_anchor(TokenStream<ComponentValue>& tokens)
{
// <anchor()> = anchor( <anchor-name>? && <anchor-side>, <length-percentage>? )
auto transaction = tokens.begin_transaction();
auto const& function_token = tokens.consume_a_token();
if (!function_token.is_function("anchor"sv))
return {};
auto argument_tokens = TokenStream { function_token.function().value };
auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.function().name });
Optional<FlyString> anchor_name;
RefPtr<CSSStyleValue const> anchor_side_value;
RefPtr<CSSStyleValue const> fallback_value;
for (auto i = 0; i < 2; ++i) {
argument_tokens.discard_whitespace();
// <anchor-name> = <dashed-ident>
if (auto dashed_ident = parse_dashed_ident(argument_tokens); dashed_ident.has_value()) {
if (anchor_name.has_value())
return {};
anchor_name = dashed_ident.value();
continue;
}
if (anchor_side_value)
break;
// <anchor-side> = inside | outside
// | top | left | right | bottom
// | start | end | self-start | self-end
// | <percentage> | center
anchor_side_value = parse_keyword_value(argument_tokens);
if (!anchor_side_value) {
// FIXME: Only percentages are allowed here, but we parse a length-percentage so that calc values are handled.
anchor_side_value = parse_length_percentage_value(argument_tokens);
if (!anchor_side_value)
return {};
if (anchor_side_value->is_length())
return {};
} else if (auto anchor_side_keyword = keyword_to_anchor_side(anchor_side_value->to_keyword()); !anchor_side_keyword.has_value()) {
return {};
}
}
if (argument_tokens.next_token().is(Token::Type::Comma)) {
argument_tokens.discard_a_token();
argument_tokens.discard_whitespace();
fallback_value = parse_length_percentage_value(argument_tokens);
if (!fallback_value) {
fallback_value = parse_anchor(argument_tokens);
if (!fallback_value)
return {};
argument_tokens.discard_a_token();
}
}
if (argument_tokens.has_next_token())
return {};
if (!anchor_side_value)
return {};
return AnchorStyleValue::create(anchor_name, anchor_side_value.release_nonnull(), fallback_value);
}
// https://drafts.csswg.org/css-anchor-position-1/#sizing // https://drafts.csswg.org/css-anchor-position-1/#sizing
RefPtr<CSSStyleValue const> Parser::parse_anchor_size(TokenStream<ComponentValue>& tokens) RefPtr<CSSStyleValue const> Parser::parse_anchor_size(TokenStream<ComponentValue>& tokens)
{ {
@ -4507,6 +4577,8 @@ NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(DOM::A
RefPtr<CSSStyleValue const> Parser::parse_value(ValueType value_type, TokenStream<ComponentValue>& tokens) RefPtr<CSSStyleValue const> Parser::parse_value(ValueType value_type, TokenStream<ComponentValue>& tokens)
{ {
switch (value_type) { switch (value_type) {
case ValueType::Anchor:
return parse_anchor(tokens);
case ValueType::AnchorSize: case ValueType::AnchorSize:
return parse_anchor_size(tokens); return parse_anchor_size(tokens);
case ValueType::Angle: case ValueType::Angle:

View file

@ -1123,7 +1123,8 @@
"initial": "auto", "initial": "auto",
"valid-types": [ "valid-types": [
"length [-∞,∞]", "length [-∞,∞]",
"percentage [-∞,∞]" "percentage [-∞,∞]",
"anchor"
], ],
"valid-identifiers": [ "valid-identifiers": [
"auto" "auto"
@ -2151,7 +2152,8 @@
"initial": "auto", "initial": "auto",
"valid-types": [ "valid-types": [
"length [-∞,∞]", "length [-∞,∞]",
"percentage [-∞,∞]" "percentage [-∞,∞]",
"anchor"
], ],
"valid-identifiers": [ "valid-identifiers": [
"auto" "auto"
@ -2862,7 +2864,8 @@
"initial": "auto", "initial": "auto",
"valid-types": [ "valid-types": [
"length [-∞,∞]", "length [-∞,∞]",
"percentage [-∞,∞]" "percentage [-∞,∞]",
"anchor"
], ],
"valid-identifiers": [ "valid-identifiers": [
"auto" "auto"
@ -3234,7 +3237,8 @@
"initial": "auto", "initial": "auto",
"valid-types": [ "valid-types": [
"length [-∞,∞]", "length [-∞,∞]",
"percentage [-∞,∞]" "percentage [-∞,∞]",
"anchor"
], ],
"valid-identifiers": [ "valid-identifiers": [
"auto" "auto"

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/StyleValues/AnchorStyleValue.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<AnchorStyleValue const> AnchorStyleValue::create(
Optional<FlyString> const& anchor_name,
ValueComparingNonnullRefPtr<CSSStyleValue const> const& anchor_side,
ValueComparingRefPtr<CSSStyleValue const> const& fallback_value)
{
return adopt_ref(*new (nothrow) AnchorStyleValue(anchor_name, anchor_side, fallback_value));
}
AnchorStyleValue::AnchorStyleValue(Optional<FlyString> const& anchor_name,
ValueComparingNonnullRefPtr<CSSStyleValue const> const& anchor_side,
ValueComparingRefPtr<CSSStyleValue const> const& fallback_value)
: StyleValueWithDefaultOperators(Type::Anchor)
, m_properties { .anchor_name = anchor_name, .anchor_side = anchor_side, .fallback_value = fallback_value }
{
}
String AnchorStyleValue::to_string(SerializationMode serialization_mode) const
{
StringBuilder builder;
builder.append("anchor("sv);
if (anchor_name().has_value())
builder.append(anchor_name().value());
if (anchor_name().has_value())
builder.append(' ');
builder.append(anchor_side()->to_string(serialization_mode));
if (fallback_value()) {
builder.append(", "sv);
builder.append(fallback_value()->to_string(serialization_mode));
}
builder.append(')');
return MUST(builder.to_string());
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/PercentageOr.h>
namespace Web::CSS {
// https://drafts.csswg.org/css-anchor-position-1/#funcdef-anchor-size
class AnchorStyleValue final : public StyleValueWithDefaultOperators<AnchorStyleValue> {
public:
static ValueComparingNonnullRefPtr<AnchorStyleValue const> create(Optional<FlyString> const& anchor_name,
ValueComparingNonnullRefPtr<CSSStyleValue const> const& anchor_side,
ValueComparingRefPtr<CSSStyleValue const> const& fallback_value);
virtual ~AnchorStyleValue() override = default;
virtual String to_string(SerializationMode) const override;
bool properties_equal(AnchorStyleValue const& other) const { return m_properties == other.m_properties; }
Optional<FlyString const&> anchor_name() const { return m_properties.anchor_name; }
ValueComparingNonnullRefPtr<CSSStyleValue const> anchor_side() const
{
return m_properties.anchor_side;
}
ValueComparingRefPtr<CSSStyleValue const> fallback_value() const
{
return m_properties.fallback_value;
}
private:
AnchorStyleValue(Optional<FlyString> const& anchor_name, ValueComparingNonnullRefPtr<CSSStyleValue const> const& anchor_side, ValueComparingRefPtr<CSSStyleValue const> const& fallback_value);
struct Properties {
Optional<FlyString> anchor_name;
ValueComparingNonnullRefPtr<CSSStyleValue const> anchor_side;
ValueComparingRefPtr<CSSStyleValue const> fallback_value;
bool operator==(Properties const&) const = default;
} m_properties;
};
}

View file

@ -12,6 +12,7 @@
namespace Web::CSS { namespace Web::CSS {
enum class ValueType : u8 { enum class ValueType : u8 {
Anchor,
AnchorSize, AnchorSize,
Angle, Angle,
BackgroundPosition, BackgroundPosition,

View file

@ -192,6 +192,7 @@ class SubtleCrypto;
namespace Web::CSS { namespace Web::CSS {
class AbstractImageStyleValue; class AbstractImageStyleValue;
class AnchorStyleValue;
class AnchorSizeStyleValue; class AnchorSizeStyleValue;
class Angle; class Angle;
class AngleOrCalculated; class AngleOrCalculated;

View file

@ -830,7 +830,9 @@ bool property_accepts_type(PropertyID property_id, ValueType value_type)
if (enum_names.contains_slow(type_name)) if (enum_names.contains_slow(type_name))
continue; continue;
if (type_name == "angle") { if (type_name == "anchor") {
property_generator.appendln(" case ValueType::Anchor:");
} else if (type_name == "angle") {
property_generator.appendln(" case ValueType::Angle:"); property_generator.appendln(" case ValueType::Angle:");
} else if (type_name == "background-position") { } else if (type_name == "background-position") {
property_generator.appendln(" case ValueType::BackgroundPosition:"); property_generator.appendln(" case ValueType::BackgroundPosition:");

View file

@ -0,0 +1,29 @@
Harness status: OK
Found 24 tests
24 Pass
Pass e.style['margin-top'] = "anchor(--foo top)" should not set the property value
Pass e.style['height'] = "anchor(--foo top)" should not set the property value
Pass e.style['font-size'] = "anchor(--foo top)" should not set the property value
Pass e.style['top'] = "anchor(--foo, top)" should not set the property value
Pass e.style['top'] = "anchor(--foo top,)" should not set the property value
Pass e.style['top'] = "anchor(--foo top bottom)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, 10px 20%)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, 10px, 20%)" should not set the property value
Pass e.style['top'] = "anchor(2 * 20%)" should not set the property value
Pass e.style['top'] = "anchor((2 * 20%))" should not set the property value
Pass e.style['top'] = "anchor(foo top)" should not set the property value
Pass e.style['top'] = "anchor(top foo)" should not set the property value
Pass e.style['top'] = "anchor(--foo height)" should not set the property value
Pass e.style['top'] = "anchor(--foo 10em)" should not set the property value
Pass e.style['top'] = "anchor(--foo 100s)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, 1)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, 100s)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, bottom)" should not set the property value
Pass e.style['top'] = "anchor(--foo top, anchor(bar top))" should not set the property value
Pass e.style['top'] = "anchor(--foo top, anchor-size(bar height))" should not set the property value
Pass e.style['top'] = "anchor(--foo top, auto" should not set the property value
Pass e.style['top'] = "calc(anchor(foo top) + 10px + 10%)" should not set the property value
Pass e.style['top'] = "calc(10px + 100 * anchor(--foo top, anchor(bar bottom)))" should not set the property value
Pass e.style['top'] = "min(anchor(--foo top), anchor(--bar bottom), anchor-size(baz height))" should not set the property value

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<title>Tests values that are invalid at parse time for the anchor() function</title>
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#anchor-pos">
<link rel="author" href="mailto:xiaochengh@chromium.org">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../css/support/parsing-testcommon.js"></script>
<script>
// anchor() can only be used in inset properties
test_invalid_value('margin-top', 'anchor(--foo top)');
test_invalid_value('height', 'anchor(--foo top)');
test_invalid_value('font-size', 'anchor(--foo top)');
// Invalid parameter format
test_invalid_value('top', 'anchor(--foo, top)');
test_invalid_value('top', 'anchor(--foo top,)');
test_invalid_value('top', 'anchor(--foo top bottom)');
test_invalid_value('top', 'anchor(--foo top, 10px 20%)');
test_invalid_value('top', 'anchor(--foo top, 10px, 20%)');
test_invalid_value('top', 'anchor(2 * 20%)');
test_invalid_value('top', 'anchor((2 * 20%))');
// Anchor name must be a dashed ident
test_invalid_value('top', 'anchor(foo top)');
test_invalid_value('top', 'anchor(top foo)');
// Invalid anchor side values
test_invalid_value('top', 'anchor(--foo height)');
test_invalid_value('top', 'anchor(--foo 10em)');
test_invalid_value('top', 'anchor(--foo 100s)');
// Invalid fallback values
test_invalid_value('top', 'anchor(--foo top, 1)');
test_invalid_value('top', 'anchor(--foo top, 100s)');
test_invalid_value('top', 'anchor(--foo top, bottom)');
test_invalid_value('top', 'anchor(--foo top, anchor(bar top))');
test_invalid_value('top', 'anchor(--foo top, anchor-size(bar height))');
test_invalid_value('top', 'anchor(--foo top, auto');
// Invalid anchor values in calc tree
test_invalid_value('top', 'calc(anchor(foo top) + 10px + 10%)');
test_invalid_value('top', 'calc(10px + 100 * anchor(--foo top, anchor(bar bottom)))');
test_invalid_value('top', 'min(anchor(--foo top), anchor(--bar bottom), anchor-size(baz height))');
</script>

View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<title>Tests parsing of the anchor() function</title>
<link rel="help" href="https://drafts.csswg.org/css-anchor-1/#anchor-pos">
<link rel="author" href="mailto:xiaochengh@chromium.org">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../css/support/parsing-testcommon.js"></script>
<script>
const anchorNames = [
'',
'--foo',
];
const insetProperties = [
'left',
'right',
'top',
'bottom',
'inset-block-start',
'inset-block-end',
'inset-inline-start',
'inset-inline-end',
];
const anchorSides = [
'inside',
'outside',
'left',
'right',
'top',
'bottom',
'start',
'end',
'self-start',
'self-end',
'center',
'50%',
'calc(50%)',
'min(50%, 100%)',
];
const fallbacks = [
null,
'1px',
'50%',
'calc(50% + 1px)',
'anchor(left)',
'anchor(--bar left)',
'anchor(--bar left, anchor(--baz right))',
];
// Tests basic combinations
for (let property of insetProperties) {
// Using a wrong anchor-side (e.g., `top: anchor(--foo left)`) doesn't cause a
// parse error, but triggers the fallback when resolved.
for (let name of anchorNames) {
for (let side of anchorSides) {
for (let fallback of fallbacks) {
let value = `anchor(${name ? name + ' ' : ''}${side}${fallback ? ', ' + fallback : ''})`;
test_valid_value(property, value);
if (name) {
// The <anchor-element> is allowed to appear after the <anchor-side>
let value_flip_order = `anchor(${side} ${name}${fallback ? ', ' + fallback : ''})`;
test_valid_value(property, value_flip_order, value);
}
}
}
}
}
// Tests that anchor() can be used in a calc tree
// Still follow the simplification process as outlined in https://drafts.csswg.org/css-values-4/#calc-simplification
test_valid_value('top', 'calc((anchor(--foo top) + anchor(--bar bottom)) / 2)', 'calc(0.5 * (anchor(--foo top) + anchor(--bar bottom)))');
test_valid_value('top', 'calc(0.5 * (anchor(--foo top) + anchor(--bar bottom)))');
test_valid_value('top', 'anchor(--foo top, calc(0.5 * anchor(--bar bottom)))');
test_valid_value('top', 'min(100px, 10%, anchor(--foo top), anchor(--bar bottom))');
</script>