LibWeb: Parse text-underline-position property

This introduces the `TextUnderlinePositionStyleValue` class, it is
possible to represent `text-underline-position` as a `StyleValueList`
but would have required ugly workarounds for either serialization or in
`ComputedProperties::text_underline_position`
This commit is contained in:
Callum Law 2025-09-13 19:27:52 +12:00 committed by Tim Ledbetter
commit b0e3af7d10
Notes: github-actions[bot] 2025-09-15 14:25:30 +00:00
21 changed files with 306 additions and 62 deletions

View file

@ -260,6 +260,7 @@ set(SOURCES
CSS/StyleValues/ShorthandStyleValue.cpp CSS/StyleValues/ShorthandStyleValue.cpp
CSS/StyleValues/StyleValue.cpp CSS/StyleValues/StyleValue.cpp
CSS/StyleValues/StyleValueList.cpp CSS/StyleValues/StyleValueList.cpp
CSS/StyleValues/TextUnderlinePositionStyleValue.cpp
CSS/StyleValues/TransformationStyleValue.cpp CSS/StyleValues/TransformationStyleValue.cpp
CSS/StyleValues/TransitionStyleValue.cpp CSS/StyleValues/TransitionStyleValue.cpp
CSS/StyleValues/UnicodeRangeStyleValue.cpp CSS/StyleValues/UnicodeRangeStyleValue.cpp

View file

@ -716,6 +716,16 @@
"none", "none",
"uppercase" "uppercase"
], ],
"text-underline-position-horizontal": [
"auto",
"from-font",
"under"
],
"text-underline-position-vertical": [
"auto",
"left",
"right"
],
"text-wrap-mode": [ "text-wrap-mode": [
"wrap", "wrap",
"nowrap" "nowrap"

View file

@ -541,6 +541,7 @@
"ui-serif", "ui-serif",
"ultra-condensed", "ultra-condensed",
"ultra-expanded", "ultra-expanded",
"under",
"underline", "underline",
"unicase", "unicase",
"unicode", "unicode",

View file

@ -472,6 +472,7 @@ private:
RefPtr<StyleValue const> parse_single_shadow_value(TokenStream<ComponentValue>&, AllowInsetKeyword); RefPtr<StyleValue const> parse_single_shadow_value(TokenStream<ComponentValue>&, AllowInsetKeyword);
RefPtr<StyleValue const> parse_text_decoration_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_decoration_line_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_rotate_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_stroke_dasharray_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_stroke_dasharray_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_easing_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_easing_value(TokenStream<ComponentValue>&);

View file

@ -57,6 +57,7 @@
#include <LibWeb/CSS/StyleValues/StringStyleValue.h> #include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h> #include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h> #include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h> #include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h> #include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransitionStyleValue.h> #include <LibWeb/CSS/StyleValues/TransitionStyleValue.h>
@ -749,6 +750,10 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
if (auto parsed_value = parse_shadow_value(tokens, AllowInsetKeyword::No); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_shadow_value(tokens, AllowInsetKeyword::No); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
return ParseError::SyntaxError; return ParseError::SyntaxError;
case PropertyID::TextUnderlinePosition:
if (auto parsed_value = parse_text_underline_position_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::TouchAction: case PropertyID::TouchAction:
if (auto parsed_value = parse_touch_action_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_touch_action_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
@ -4283,6 +4288,51 @@ RefPtr<StyleValue const> Parser::parse_text_decoration_line_value(TokenStream<Co
return StyleValueList::create(move(style_values), StyleValueList::Separator::Space); return StyleValueList::create(move(style_values), StyleValueList::Separator::Space);
} }
// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
RefPtr<StyleValue const> Parser::parse_text_underline_position_value(TokenStream<ComponentValue>& tokens)
{
// auto | [ from-font | under ] || [ left | right ]
auto transaction = tokens.begin_transaction();
if (parse_all_as_single_keyword_value(tokens, Keyword::Auto)) {
transaction.commit();
return TextUnderlinePositionStyleValue::create(TextUnderlinePositionHorizontal::Auto, TextUnderlinePositionVertical::Auto);
}
Optional<TextUnderlinePositionHorizontal> horizontal_value;
Optional<TextUnderlinePositionVertical> vertical_value;
while (tokens.has_next_token()) {
auto keyword_value = parse_keyword_value(tokens);
if (!keyword_value)
return nullptr;
if (auto maybe_horizontal_value = keyword_to_text_underline_position_horizontal(keyword_value->to_keyword()); maybe_horizontal_value.has_value()) {
if (maybe_horizontal_value == TextUnderlinePositionHorizontal::Auto || horizontal_value.has_value())
return nullptr;
horizontal_value = maybe_horizontal_value;
continue;
}
if (auto maybe_vertical_value = keyword_to_text_underline_position_vertical(keyword_value->to_keyword()); maybe_vertical_value.has_value()) {
if (maybe_vertical_value == TextUnderlinePositionVertical::Auto || vertical_value.has_value())
return nullptr;
vertical_value = maybe_vertical_value;
continue;
}
return nullptr;
}
transaction.commit();
return TextUnderlinePositionStyleValue::create(horizontal_value.value_or(TextUnderlinePositionHorizontal::Auto), vertical_value.value_or(TextUnderlinePositionVertical::Auto));
}
// https://www.w3.org/TR/pointerevents/#the-touch-action-css-property // https://www.w3.org/TR/pointerevents/#the-touch-action-css-property
RefPtr<StyleValue const> Parser::parse_touch_action_value(TokenStream<ComponentValue>& tokens) RefPtr<StyleValue const> Parser::parse_touch_action_value(TokenStream<ComponentValue>& tokens)
{ {

View file

@ -3383,6 +3383,15 @@
], ],
"percentages-resolve-to": "length" "percentages-resolve-to": "length"
}, },
"text-underline-position": {
"animation-type": "discrete",
"inherited": true,
"initial": "auto",
"valid-types": [
"text-underline-position-horizontal",
"text-underline-position-vertical"
]
},
"text-wrap": { "text-wrap": {
"inherited": true, "inherited": true,
"initial": "wrap", "initial": "wrap",

View file

@ -66,6 +66,7 @@
#include <LibWeb/CSS/StyleValues/StringStyleValue.h> #include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h> #include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h> #include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h> #include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h> #include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransitionStyleValue.h> #include <LibWeb/CSS/StyleValues/TransitionStyleValue.h>

View file

@ -82,6 +82,7 @@ namespace Web::CSS {
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Shadow, shadow, ShadowStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(Shadow, shadow, ShadowStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Shorthand, shorthand, ShorthandStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(Shorthand, shorthand, ShorthandStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(String, string, StringStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(String, string, StringStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(TextUnderlinePosition, text_underline_position, TextUnderlinePositionStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Time, time, TimeStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(Time, time, TimeStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Transformation, transformation, TransformationStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(Transformation, transformation, TransformationStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Transition, transition, TransitionStyleValue) \ __ENUMERATE_CSS_STYLE_VALUE_TYPE(Transition, transition, TransitionStyleValue) \

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/StyleValues/TextUnderlinePositionStyleValue.h>
namespace Web::CSS {
String TextUnderlinePositionStyleValue::to_string(SerializationMode) const
{
if (m_horizontal == TextUnderlinePositionHorizontal::Auto && m_vertical == TextUnderlinePositionVertical::Auto)
return "auto"_string;
if (m_vertical == TextUnderlinePositionVertical::Auto)
return MUST(String::from_utf8(CSS::to_string(m_horizontal)));
if (m_horizontal == TextUnderlinePositionHorizontal::Auto)
return MUST(String::from_utf8(CSS::to_string(m_vertical)));
return MUST(String::formatted("{} {}", CSS::to_string(m_horizontal), CSS::to_string(m_vertical)));
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::CSS {
class TextUnderlinePositionStyleValue : public StyleValueWithDefaultOperators<TextUnderlinePositionStyleValue> {
public:
static ValueComparingNonnullRefPtr<TextUnderlinePositionStyleValue const> create(TextUnderlinePositionHorizontal horizontal, TextUnderlinePositionVertical vertical)
{
return adopt_ref(*new (nothrow) TextUnderlinePositionStyleValue(horizontal, vertical));
}
virtual ~TextUnderlinePositionStyleValue() override = default;
TextUnderlinePositionHorizontal horizontal() const { return m_horizontal; }
TextUnderlinePositionVertical vertical() const { return m_vertical; }
virtual String to_string(SerializationMode serialization_mode) const override;
bool properties_equal(TextUnderlinePositionStyleValue const& other) const { return m_horizontal == other.m_horizontal && m_vertical == other.m_vertical; }
private:
explicit TextUnderlinePositionStyleValue(TextUnderlinePositionHorizontal horizontal, TextUnderlinePositionVertical vertical)
: StyleValueWithDefaultOperators(Type::TextUnderlinePosition)
, m_horizontal(horizontal)
, m_vertical(vertical)
{
}
TextUnderlinePositionHorizontal m_horizontal;
TextUnderlinePositionVertical m_vertical;
};
}

View file

@ -364,6 +364,7 @@ class StyleValue;
class StyleValueList; class StyleValueList;
class Supports; class Supports;
class SVGPaint; class SVGPaint;
class TextUnderlinePositionStyleValue;
class Time; class Time;
class TimeOrCalculated; class TimeOrCalculated;
class TimePercentage; class TimePercentage;

View file

@ -63,6 +63,7 @@ All properties associated with getComputedStyle(document.body):
"text-shadow", "text-shadow",
"text-transform", "text-transform",
"text-underline-offset", "text-underline-offset",
"text-underline-position",
"text-wrap-mode", "text-wrap-mode",
"text-wrap-style", "text-wrap-style",
"visibility", "visibility",

View file

@ -691,6 +691,8 @@ All supported properties and their default values exposed from CSSStylePropertie
'text-transform': 'none' 'text-transform': 'none'
'textUnderlineOffset': 'auto' 'textUnderlineOffset': 'auto'
'text-underline-offset': 'auto' 'text-underline-offset': 'auto'
'textUnderlinePosition': 'auto'
'text-underline-position': 'auto'
'textWrap': 'wrap' 'textWrap': 'wrap'
'text-wrap': 'wrap' 'text-wrap': 'wrap'
'textWrapMode': 'wrap' 'textWrapMode': 'wrap'

View file

@ -61,6 +61,7 @@ text-rendering: auto
text-shadow: none text-shadow: none
text-transform: none text-transform: none
text-underline-offset: auto text-underline-offset: auto
text-underline-position: auto
text-wrap-mode: wrap text-wrap-mode: wrap
text-wrap-style: auto text-wrap-style: auto
visibility: visible visibility: visible
@ -93,7 +94,7 @@ background-position-x: 0%
background-position-y: 0% background-position-y: 0%
background-repeat: repeat background-repeat: repeat
background-size: auto background-size: auto
block-size: 1425px block-size: 1440px
border-block-end-color: rgb(0, 0, 0) border-block-end-color: rgb(0, 0, 0)
border-block-end-style: none border-block-end-style: none
border-block-end-width: 0px border-block-end-width: 0px
@ -170,7 +171,7 @@ grid-row-start: auto
grid-template-areas: none grid-template-areas: none
grid-template-columns: none grid-template-columns: none
grid-template-rows: none grid-template-rows: none
height: 2580px height: 2595px
inline-size: 784px inline-size: 784px
inset-block-end: auto inset-block-end: auto
inset-block-start: auto inset-block-start: auto

View file

@ -1,8 +1,8 @@
Harness status: OK Harness status: OK
Found 259 tests Found 260 tests
252 Pass 253 Pass
7 Fail 7 Fail
Pass accent-color Pass accent-color
Pass border-collapse Pass border-collapse
@ -65,6 +65,7 @@ Pass text-rendering
Pass text-shadow Pass text-shadow
Pass text-transform Pass text-transform
Pass text-underline-offset Pass text-underline-offset
Pass text-underline-position
Pass text-wrap-mode Pass text-wrap-mode
Pass text-wrap-style Pass text-wrap-style
Pass visibility Pass visibility

View file

@ -0,0 +1,12 @@
Harness status: OK
Found 7 tests
7 Pass
Pass Property text-underline-position value 'auto'
Pass Property text-underline-position value 'under'
Pass Property text-underline-position value 'from-font'
Pass Property text-underline-position value 'left'
Pass Property text-underline-position value 'right'
Pass Property text-underline-position value 'under left'
Pass Property text-underline-position value 'from-font left'

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass e.style['text-underline-position'] = "auto under" should not set the property value
Pass e.style['text-underline-position'] = "auto from-font" should not set the property value
Pass e.style['text-underline-position'] = "left auto" should not set the property value
Pass e.style['text-underline-position'] = "left right" should not set the property value
Pass e.style['text-underline-position'] = "right under left" should not set the property value
Pass e.style['text-underline-position'] = "under from-font" should not set the property value

View file

@ -0,0 +1,14 @@
Harness status: OK
Found 9 tests
9 Pass
Pass e.style['text-underline-position'] = "auto" should set the property value
Pass e.style['text-underline-position'] = "under" should set the property value
Pass e.style['text-underline-position'] = "from-font" should set the property value
Pass e.style['text-underline-position'] = "left" should set the property value
Pass e.style['text-underline-position'] = "right" should set the property value
Pass e.style['text-underline-position'] = "under left" should set the property value
Pass e.style['text-underline-position'] = "from-font left" should set the property value
Pass e.style['text-underline-position'] = "right under" should set the property value
Pass e.style['text-underline-position'] = "right from-font" should set the property value

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSS Text Decoration Test: getComputedStyle().textUnderlinePosition</title>
<link rel="help" href="https://drafts.csswg.org/css-text-decor-3/#text-decoration-style-property">
<meta name="assert" content="text-underline-position computed value is as specified.">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/computed-testcommon.js"></script>
</head>
<body>
<div id="target"></div>
<script>
test_computed_value("text-underline-position", "auto");
test_computed_value("text-underline-position", "under");
test_computed_value("text-underline-position", "from-font");
test_computed_value("text-underline-position", "left");
test_computed_value("text-underline-position", "right");
test_computed_value("text-underline-position", "under left");
test_computed_value("text-underline-position", "from-font left");
</script>
</body>
</html>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Text Decoration Test: Parsing text-underline-position with invalid values</title>
<link rel="help" href="https://drafts.csswg.org/css-text-decor-3/#text-decoration-style-property">
<meta name="assert" content="text-underline-position supports only the grammar 'auto | [ under || [ left | right ] ]'.">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
<script>
test_invalid_value("text-underline-position", "auto under");
test_invalid_value("text-underline-position", "auto from-font");
test_invalid_value("text-underline-position", "left auto");
test_invalid_value("text-underline-position", "left right");
test_invalid_value("text-underline-position", "right under left");
test_invalid_value("text-underline-position", "under from-font");
</script>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Text Decoration Test: Parsing text-underline-position with valid values</title>
<link rel="help" href="https://drafts.csswg.org/css-text-decor-3/#text-decoration-style-property">
<meta name="assert" content="text-underline-position supports the full grammar 'auto | [ under || [ left | right ] ]'.">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
<script>
test_valid_value("text-underline-position", "auto");
test_valid_value("text-underline-position", "under");
test_valid_value("text-underline-position", "from-font");
test_valid_value("text-underline-position", "left");
test_valid_value("text-underline-position", "right");
test_valid_value("text-underline-position", "under left");
test_valid_value("text-underline-position", "from-font left");
test_valid_value("text-underline-position", "right under", "under right");
test_valid_value("text-underline-position", "right from-font", "from-font right");
</script>