From c4b9e7eadffe6ef4d6bc9ff04da1e027a47f818f Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 18 Nov 2025 10:53:25 +0000 Subject: [PATCH] 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. --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/CSS/ComputedProperties.cpp | 12 +++++ Libraries/LibWeb/CSS/ComputedProperties.h | 1 + Libraries/LibWeb/CSS/ComputedValues.h | 14 +++-- Libraries/LibWeb/CSS/Interpolation.cpp | 17 ++++++ Libraries/LibWeb/CSS/Keywords.json | 2 + Libraries/LibWeb/CSS/Parser/Parser.h | 1 + .../LibWeb/CSS/Parser/PropertyParsing.cpp | 48 +++++++++++++++++ Libraries/LibWeb/CSS/Properties.json | 4 ++ .../LibWeb/CSS/StyleValues/StyleValue.cpp | 2 +- Libraries/LibWeb/CSS/StyleValues/StyleValue.h | 1 + .../CSS/StyleValues/TextIndentStyleValue.cpp | 54 +++++++++++++++++++ .../CSS/StyleValues/TextIndentStyleValue.h | 43 +++++++++++++++ Libraries/LibWeb/Forward.h | 1 + Libraries/LibWeb/Layout/LineBuilder.cpp | 3 +- Libraries/LibWeb/Layout/Node.cpp | 4 +- 16 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.cpp create mode 100644 Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index e4e89152e0e..e6e0ae848c7 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 356507b4320..845e0b67e6e 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -1369,6 +1370,17 @@ Vector 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); diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index 7003b6cf067..8c5fc48bc2a 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -122,6 +122,7 @@ public: TextDecorationThickness text_decoration_thickness() const; TextTransform text_transform() const; Vector 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; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index b2ffe3c8369..9042569957f 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -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 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&& 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; } diff --git a/Libraries/LibWeb/CSS/Interpolation.cpp b/Libraries/LibWeb/CSS/Interpolation.cpp index 01cadfb8677..df9182407fe 100644 --- a/Libraries/LibWeb/CSS/Interpolation.cpp +++ b/Libraries/LibWeb/CSS/Interpolation.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -1877,6 +1878,22 @@ static RefPtr 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 diff --git a/Libraries/LibWeb/CSS/Keywords.json b/Libraries/LibWeb/CSS/Keywords.json index e764db9bd34..6bbe2eea4c5 100644 --- a/Libraries/LibWeb/CSS/Keywords.json +++ b/Libraries/LibWeb/CSS/Keywords.json @@ -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", diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 985c1d2f7ea..3fef44412e9 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -506,6 +506,7 @@ private: RefPtr parse_shape_outside_value(TokenStream&); RefPtr parse_text_decoration_value(TokenStream&); RefPtr parse_text_decoration_line_value(TokenStream&); + RefPtr parse_text_indent_value(TokenStream&); RefPtr parse_text_underline_position_value(TokenStream&); RefPtr parse_rotate_value(TokenStream&); RefPtr parse_stroke_dasharray_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp index 34e10d2745a..4b6f669cc14 100644 --- a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -721,6 +722,8 @@ Parser::ParseErrorOr> 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 Parser::parse_text_decoration_line_value(TokenStream Parser::parse_text_indent_value(TokenStream& tokens) +{ + // [ ] && hanging? && each-line? + auto transaction = tokens.begin_transaction(); + + RefPtr 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 Parser::parse_text_underline_position_value(TokenStream& tokens) { diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 0f9c9db13e2..3c12b9d6059 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -3680,6 +3680,10 @@ "length [-∞,∞]", "percentage [-∞,∞]" ], + "valid-identifiers": [ + "each-line", + "hanging" + ], "percentages-resolve-to": "length", "quirks": [ "unitless-length" diff --git a/Libraries/LibWeb/CSS/StyleValues/StyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/StyleValue.cpp index 9863b93ae7b..edd902ef21a 100644 --- a/Libraries/LibWeb/CSS/StyleValues/StyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/StyleValue.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -68,6 +67,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/CSS/StyleValues/StyleValue.h b/Libraries/LibWeb/CSS/StyleValues/StyleValue.h index 3117de65c94..4adeb032662 100644 --- a/Libraries/LibWeb/CSS/StyleValues/StyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/StyleValue.h @@ -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) \ diff --git a/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.cpp new file mode 100644 index 00000000000..62db7cf6db4 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TextIndentStyleValue.h" + +namespace Web::CSS { + +ValueComparingNonnullRefPtr TextIndentStyleValue::create(NonnullRefPtr length_percentage, Hanging hanging, EachLine each_line) +{ + return adopt_ref(*new (nothrow) TextIndentStyleValue(move(length_percentage), hanging, each_line)); +} + +TextIndentStyleValue::TextIndentStyleValue(NonnullRefPtr 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 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; +} + +} diff --git a/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.h new file mode 100644 index 00000000000..e74d1840300 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/TextIndentStyleValue.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +class TextIndentStyleValue : public StyleValueWithDefaultOperators { +public: + enum class Hanging : u8 { + No, + Yes, + }; + enum class EachLine : u8 { + No, + Yes, + }; + + static ValueComparingNonnullRefPtr create(NonnullRefPtr 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 absolutized(ComputationContext const&) const override; + bool properties_equal(TextIndentStyleValue const&) const; + +private: + TextIndentStyleValue(NonnullRefPtr length_percentage, Hanging hanging, EachLine each_line); + + NonnullRefPtr m_length_percentage; + bool m_hanging; + bool m_each_line; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 51989292539..02fe168d95b 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -378,6 +378,7 @@ class StyleValueList; class SuperellipseStyleValue; class Supports; class SVGPaint; +class TextIndentStyleValue; class TextUnderlinePositionStyleValue; class Time; class TimeOrCalculated; diff --git a/Libraries/LibWeb/Layout/LineBuilder.cpp b/Libraries/LibWeb/Layout/LineBuilder.cpp index bd9202d5c9c..1f90d705968 100644 --- a/Libraries/LibWeb/Layout/LineBuilder.cpp +++ b/Libraries/LibWeb/Layout/LineBuilder.cpp @@ -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); } diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 740a8221bef..7fb47a268fd 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -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());