LibWeb: Parse and propagate extended text-indent property values

CSS Text 3 gives `text-indent` a couple of optional keywords to control
which lines are affected. This commit parses them, but doesn't yet do
anything with them.
This commit is contained in:
Sam Atkins 2025-11-18 10:53:25 +00:00 committed by Jelle Raaijmakers
parent eea1d4e1d2
commit c4b9e7eadf
Notes: github-actions[bot] 2025-11-20 15:03:53 +00:00
16 changed files with 199 additions and 9 deletions

View file

@ -273,6 +273,7 @@ set(SOURCES
CSS/StyleValues/StyleValue.cpp
CSS/StyleValues/StyleValueList.cpp
CSS/StyleValues/SuperellipseStyleValue.cpp
CSS/StyleValues/TextIndentStyleValue.cpp
CSS/StyleValues/TextUnderlinePositionStyleValue.cpp
CSS/StyleValues/TransformationStyleValue.cpp
CSS/StyleValues/TreeCountingFunctionStyleValue.cpp

View file

@ -36,6 +36,7 @@
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TextIndentStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
@ -1369,6 +1370,17 @@ Vector<ShadowData> ComputedProperties::text_shadow(Layout::Node const& layout_no
return shadow(PropertyID::TextShadow, layout_node);
}
TextIndentData ComputedProperties::text_indent() const
{
auto const& value = property(PropertyID::TextIndent).as_text_indent();
return TextIndentData {
.length_percentage = LengthPercentage::from_style_value(value.length_percentage()),
.each_line = value.each_line(),
.hanging = value.hanging(),
};
}
TextWrapMode ComputedProperties::text_wrap_mode() const
{
auto const& value = property(PropertyID::TextWrapMode);

View file

@ -122,6 +122,7 @@ public:
TextDecorationThickness text_decoration_thickness() const;
TextTransform text_transform() const;
Vector<ShadowData> text_shadow(Layout::Node const&) const;
TextIndentData text_indent() const;
TextWrapMode text_wrap_mode() const;
ListStyleType list_style_type() const;
ListStylePosition list_style_position() const;

View file

@ -94,6 +94,12 @@ struct ScrollbarColorData {
Color track_color { Color::Transparent };
};
struct TextIndentData {
LengthPercentage length_percentage;
bool each_line { false };
bool hanging { false };
};
struct TextUnderlinePosition {
TextUnderlinePositionHorizontal horizontal { TextUnderlinePositionHorizontal::Auto };
TextUnderlinePositionVertical vertical { TextUnderlinePositionVertical::Auto };
@ -159,7 +165,7 @@ public:
static TextDecorationStyle text_decoration_style() { return TextDecorationStyle::Solid; }
static TextTransform text_transform() { return TextTransform::None; }
static TextOverflow text_overflow() { return TextOverflow::Clip; }
static LengthPercentage text_indent() { return Length::make_px(0); }
static TextIndentData text_indent() { return { Length::make_px(0) }; }
static TextWrapMode text_wrap_mode() { return TextWrapMode::Wrap; }
static CSSPixels text_underline_offset() { return 2; }
static TextUnderlinePosition text_underline_position() { return { .horizontal = TextUnderlinePositionHorizontal::Auto, .vertical = TextUnderlinePositionVertical::Auto }; }
@ -497,7 +503,7 @@ public:
Variant<Length, double> tab_size() const { return m_inherited.tab_size; }
TextAlign text_align() const { return m_inherited.text_align; }
TextJustify text_justify() const { return m_inherited.text_justify; }
LengthPercentage const& text_indent() const { return m_inherited.text_indent; }
TextIndentData const& text_indent() const { return m_inherited.text_indent; }
TextWrapMode text_wrap_mode() const { return m_inherited.text_wrap_mode; }
CSSPixels text_underline_offset() const { return m_inherited.text_underline_offset; }
TextUnderlinePosition text_underline_position() const { return m_inherited.text_underline_position; }
@ -702,7 +708,7 @@ protected:
TextAlign text_align { InitialValues::text_align() };
TextJustify text_justify { InitialValues::text_justify() };
TextTransform text_transform { InitialValues::text_transform() };
LengthPercentage text_indent { InitialValues::text_indent() };
TextIndentData text_indent { InitialValues::text_indent() };
TextWrapMode text_wrap_mode { InitialValues::text_wrap_mode() };
CSSPixels text_underline_offset { InitialValues::text_underline_offset() };
TextUnderlinePosition text_underline_position { InitialValues::text_underline_position() };
@ -915,7 +921,7 @@ public:
void set_text_decoration_color(Color value) { m_noninherited.text_decoration_color = value; }
void set_text_transform(TextTransform value) { m_inherited.text_transform = value; }
void set_text_shadow(Vector<ShadowData>&& value) { m_inherited.text_shadow = move(value); }
void set_text_indent(LengthPercentage value) { m_inherited.text_indent = move(value); }
void set_text_indent(TextIndentData value) { m_inherited.text_indent = move(value); }
void set_text_wrap_mode(TextWrapMode value) { m_inherited.text_wrap_mode = value; }
void set_text_overflow(TextOverflow value) { m_noninherited.text_overflow = value; }
void set_text_underline_offset(CSSPixels value) { m_inherited.text_underline_offset = value; }

View file

@ -32,6 +32,7 @@
#include <LibWeb/CSS/StyleValues/RectStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/SuperellipseStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextIndentStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/Transformation.h>
@ -1877,6 +1878,22 @@ static RefPtr<StyleValue const> interpolate_value_impl(DOM::Element& element, Ca
interpolate_length_or_auto(from_rect.left_edge, to_rect.left_edge, calculation_context, delta),
});
}
case StyleValue::Type::TextIndent: {
auto& from_text_indent = from.as_text_indent();
auto& to_text_indent = to.as_text_indent();
if (from_text_indent.each_line() != to_text_indent.each_line()
|| from_text_indent.hanging() != to_text_indent.hanging())
return {};
auto interpolated_length_percentage = interpolate_value(element, calculation_context, from_text_indent.length_percentage(), to_text_indent.length_percentage(), delta, allow_discrete);
if (!interpolated_length_percentage)
return {};
return TextIndentStyleValue::create(interpolated_length_percentage.release_nonnull(),
from_text_indent.hanging() ? TextIndentStyleValue::Hanging::Yes : TextIndentStyleValue::Hanging::No,
from_text_indent.each_line() ? TextIndentStyleValue::EachLine::Yes : TextIndentStyleValue::EachLine::No);
}
case StyleValue::Type::Superellipse: {
// https://drafts.csswg.org/css-borders-4/#corner-shape-interpolation

View file

@ -187,6 +187,7 @@
"down",
"e",
"e-resize",
"each-line",
"ease",
"ease-in",
"ease-in-out",
@ -237,6 +238,7 @@
"graytext",
"grid",
"groove",
"hanging",
"hard-light",
"height",
"help",

View file

@ -506,6 +506,7 @@ private:
RefPtr<StyleValue const> parse_shape_outside_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_text_decoration_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_text_decoration_line_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_text_indent_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_text_underline_position_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_rotate_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_stroke_dasharray_value(TokenStream<ComponentValue>&);

View file

@ -57,6 +57,7 @@
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TextIndentStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
@ -721,6 +722,8 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
return parse_all_as(tokens, [this](auto& tokens) { return parse_text_decoration_value(tokens); });
case PropertyID::TextDecorationLine:
return parse_all_as(tokens, [this](auto& tokens) { return parse_text_decoration_line_value(tokens); });
case PropertyID::TextIndent:
return parse_all_as(tokens, [this](auto& tokens) { return parse_text_indent_value(tokens); });
case PropertyID::TextShadow:
return parse_all_as(tokens, [this](auto& tokens) { return parse_shadow_value(tokens, ShadowStyleValue::ShadowType::Text); });
case PropertyID::TextUnderlinePosition:
@ -4731,6 +4734,51 @@ RefPtr<StyleValue const> Parser::parse_text_decoration_line_value(TokenStream<Co
return StyleValueList::create(move(style_values), StyleValueList::Separator::Space);
}
// https://drafts.csswg.org/css-text-3/#text-indent-property
RefPtr<StyleValue const> Parser::parse_text_indent_value(TokenStream<ComponentValue>& tokens)
{
// [ <length-percentage> ] && hanging? && each-line?
auto transaction = tokens.begin_transaction();
RefPtr<StyleValue const> length_percentage;
bool has_hanging = false;
bool has_each_line = false;
tokens.discard_whitespace();
while (tokens.has_next_token()) {
if (!length_percentage) {
if (auto parsed = parse_length_percentage_value(tokens)) {
length_percentage = parsed.release_nonnull();
tokens.discard_whitespace();
continue;
}
}
if (auto keyword = parse_keyword_value(tokens)) {
if (!has_hanging && keyword->to_keyword() == Keyword::Hanging) {
has_hanging = true;
continue;
}
if (!has_each_line && keyword->to_keyword() == Keyword::EachLine) {
has_each_line = true;
continue;
}
return nullptr;
}
return nullptr;
}
if (!length_percentage)
return nullptr;
transaction.commit();
return TextIndentStyleValue::create(length_percentage.release_nonnull(),
has_hanging ? TextIndentStyleValue::Hanging::Yes : TextIndentStyleValue::Hanging::No,
has_each_line ? TextIndentStyleValue::EachLine::Yes : TextIndentStyleValue::EachLine::No);
}
// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
RefPtr<StyleValue const> Parser::parse_text_underline_position_value(TokenStream<ComponentValue>& tokens)
{

View file

@ -3680,6 +3680,10 @@
"length [-∞,∞]",
"percentage [-∞,∞]"
],
"valid-identifiers": [
"each-line",
"hanging"
],
"percentages-resolve-to": "length",
"quirks": [
"unitless-length"

View file

@ -9,7 +9,6 @@
#include <LibGfx/Font/Font.h>
#include <LibGfx/Font/FontStyleMapping.h>
#include <LibGfx/Font/FontWeight.h>
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Parser/Parser.h>
@ -68,6 +67,7 @@
#include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/SuperellipseStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextIndentStyleValue.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>

View file

@ -83,6 +83,7 @@ namespace Web::CSS {
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Shorthand, shorthand, ShorthandStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(String, string, StringStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Superellipse, superellipse, SuperellipseStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(TextIndent, text_indent, TextIndentStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(TextUnderlinePosition, text_underline_position, TextUnderlinePositionStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Time, time, TimeStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Transformation, transformation, TransformationStyleValue) \

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TextIndentStyleValue.h"
namespace Web::CSS {
ValueComparingNonnullRefPtr<TextIndentStyleValue const> TextIndentStyleValue::create(NonnullRefPtr<StyleValue const> length_percentage, Hanging hanging, EachLine each_line)
{
return adopt_ref(*new (nothrow) TextIndentStyleValue(move(length_percentage), hanging, each_line));
}
TextIndentStyleValue::TextIndentStyleValue(NonnullRefPtr<StyleValue const> length_percentage, Hanging hanging, EachLine each_line)
: StyleValueWithDefaultOperators(Type::TextIndent)
, m_length_percentage(move(length_percentage))
, m_hanging(hanging == Hanging::Yes)
, m_each_line(each_line == EachLine::Yes)
{
}
TextIndentStyleValue::~TextIndentStyleValue() = default;
String TextIndentStyleValue::to_string(SerializationMode mode) const
{
StringBuilder builder;
builder.append(m_length_percentage->to_string(mode));
if (m_each_line)
builder.append(" each-line"sv);
if (m_hanging)
builder.append(" hanging"sv);
return builder.to_string_without_validation();
}
ValueComparingNonnullRefPtr<StyleValue const> TextIndentStyleValue::absolutized(ComputationContext const& context) const
{
auto new_length_percentage = m_length_percentage->absolutized(context);
if (new_length_percentage->equals(m_length_percentage))
return *this;
return create(move(new_length_percentage),
m_hanging ? Hanging::Yes : Hanging::No,
m_each_line ? EachLine::Yes : EachLine::No);
}
bool TextIndentStyleValue::properties_equal(TextIndentStyleValue const& other) const
{
return m_length_percentage == other.m_length_percentage
&& m_each_line == other.m_each_line
&& m_hanging == other.m_hanging;
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::CSS {
class TextIndentStyleValue : public StyleValueWithDefaultOperators<TextIndentStyleValue> {
public:
enum class Hanging : u8 {
No,
Yes,
};
enum class EachLine : u8 {
No,
Yes,
};
static ValueComparingNonnullRefPtr<TextIndentStyleValue const> create(NonnullRefPtr<StyleValue const> length_percentage, Hanging hanging, EachLine each_line);
virtual ~TextIndentStyleValue() override;
StyleValue const& length_percentage() const { return m_length_percentage; }
bool hanging() const { return m_hanging; }
bool each_line() const { return m_each_line; }
virtual String to_string(SerializationMode) const override;
virtual ValueComparingNonnullRefPtr<StyleValue const> absolutized(ComputationContext const&) const override;
bool properties_equal(TextIndentStyleValue const&) const;
private:
TextIndentStyleValue(NonnullRefPtr<StyleValue const> length_percentage, Hanging hanging, EachLine each_line);
NonnullRefPtr<StyleValue const> m_length_percentage;
bool m_hanging;
bool m_each_line;
};
}

View file

@ -378,6 +378,7 @@ class StyleValueList;
class SuperellipseStyleValue;
class Supports;
class SVGPaint;
class TextIndentStyleValue;
class TextUnderlinePositionStyleValue;
class Time;
class TimeOrCalculated;

View file

@ -17,7 +17,8 @@ LineBuilder::LineBuilder(InlineFormattingContext& context, LayoutState& layout_s
, m_direction(direction)
, m_writing_mode(writing_mode)
{
m_text_indent = m_context.containing_block().computed_values().text_indent().to_px(m_context.containing_block(), m_containing_block_used_values.content_width());
auto text_indent = m_context.containing_block().computed_values().text_indent();
m_text_indent = text_indent.length_percentage.to_px(m_context.containing_block(), m_containing_block_used_values.content_width());
begin_new_line(false);
}

View file

@ -622,9 +622,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_text_underline_offset(computed_style.text_underline_offset());
computed_values.set_text_underline_position(computed_style.text_underline_position());
if (auto text_indent = computed_style.length_percentage(CSS::PropertyID::TextIndent, *this, CSS::ComputedProperties::ClampNegativeLengths::No); text_indent.has_value())
computed_values.set_text_indent(text_indent.release_value());
computed_values.set_text_indent(computed_style.text_indent());
computed_values.set_text_wrap_mode(computed_style.text_wrap_mode());
computed_values.set_tab_size(computed_style.tab_size());