Browse Source

LibWeb: Parse CSS `background-position` property

This is done a bit differently from other properties: using a
TokenStream instead of just a Vector of ComponentValues. The reason for
this is, we can then use call the same function when parsing the
`background` shorthand. Otherwise, we would have to know in advance how
many values to pass down, which basically would involve duplicating the
`background-position` parsing code inside `background`.

The StyleValue is PositionStyleValue, since it represents a
`<position>`: https://www.w3.org/TR/css-values-4/#typedef-position
Unfortunately, background-position's parsing is a bit different from
`<position>`'s, (background-position allows 3-value syntax and
`<position>` doesn't) so we'll need to come back and write a different
parsing function for that later.
Sam Atkins 3 years ago
parent
commit
988a8ed3d8

+ 173 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -2487,6 +2487,175 @@ RefPtr<StyleValue> Parser::parse_background_image_value(ParsingContext const& co
     return nullptr;
 }
 
+RefPtr<StyleValue> Parser::parse_single_background_position_value(ParsingContext const& context, TokenStream<StyleComponentValueRule>& tokens)
+{
+    // NOTE: This *looks* like it parses a <position>, but it doesn't. From the spec:
+    //      "Note: The background-position property also accepts a three-value syntax.
+    //       This has been disallowed generically because it creates parsing ambiguities
+    //       when combined with other length or percentage components in a property value."
+    //           - https://www.w3.org/TR/css-values-4/#typedef-position
+    //       So, we'll need a separate function to parse <position> later.
+
+    auto start_position = tokens.position();
+    auto error = [&]() {
+        tokens.rewind_to_position(start_position);
+        return nullptr;
+    };
+
+    auto to_edge = [](ValueID identifier) -> Optional<PositionEdge> {
+        switch (identifier) {
+        case ValueID::Top:
+            return PositionEdge::Top;
+        case ValueID::Bottom:
+            return PositionEdge::Bottom;
+        case ValueID::Left:
+            return PositionEdge::Left;
+        case ValueID::Right:
+            return PositionEdge::Right;
+        default:
+            return {};
+        }
+    };
+    auto is_horizontal = [](ValueID identifier) -> bool {
+        switch (identifier) {
+        case ValueID::Left:
+        case ValueID::Right:
+            return true;
+        default:
+            return false;
+        }
+    };
+    auto is_vertical = [](ValueID identifier) -> bool {
+        switch (identifier) {
+        case ValueID::Top:
+        case ValueID::Bottom:
+            return true;
+        default:
+            return false;
+        }
+    };
+
+    auto zero_offset = Length::make_px(0);
+    auto center_offset = Length { 50, Length::Type::Percentage };
+
+    struct EdgeOffset {
+        PositionEdge edge;
+        Length offset;
+        bool edge_provided;
+        bool offset_provided;
+    };
+
+    Optional<EdgeOffset> horizontal;
+    Optional<EdgeOffset> vertical;
+    bool found_center = false;
+
+    while (tokens.has_next_token()) {
+        // Check if we're done
+        auto seen_items = (horizontal.has_value() ? 1 : 0) + (vertical.has_value() ? 1 : 0) + (found_center ? 1 : 0);
+        if (seen_items == 2)
+            break;
+
+        auto& token = tokens.peek_token();
+        auto maybe_value = parse_css_value(context, token);
+        if (!maybe_value || !property_accepts_value(PropertyID::BackgroundPosition, *maybe_value))
+            break;
+        tokens.next_token();
+        auto value = maybe_value.release_nonnull();
+
+        if (value->has_length()) {
+            if (!horizontal.has_value()) {
+                horizontal = EdgeOffset { PositionEdge::Left, value->to_length(), false, true };
+            } else if (!vertical.has_value()) {
+                vertical = EdgeOffset { PositionEdge::Top, value->to_length(), false, true };
+            } else {
+                return error();
+            }
+            continue;
+        }
+
+        if (value->has_identifier()) {
+            auto identifier = value->to_identifier();
+            if (is_horizontal(identifier)) {
+                Length offset = zero_offset;
+                bool offset_provided = false;
+                if (tokens.has_next_token()) {
+                    auto maybe_offset = parse_length(context, tokens.peek_token());
+                    if (maybe_offset.has_value()) {
+                        offset = maybe_offset.value();
+                        offset_provided = true;
+                        tokens.next_token();
+                    }
+                }
+                horizontal = EdgeOffset { *to_edge(identifier), offset, true, offset_provided };
+            } else if (is_vertical(identifier)) {
+                Length offset = zero_offset;
+                bool offset_provided = false;
+                if (tokens.has_next_token()) {
+                    auto maybe_offset = parse_length(context, tokens.peek_token());
+                    if (maybe_offset.has_value()) {
+                        offset = maybe_offset.value();
+                        offset_provided = true;
+                        tokens.next_token();
+                    }
+                }
+                vertical = EdgeOffset { *to_edge(identifier), offset, true, offset_provided };
+            } else if (identifier == ValueID::Center) {
+                found_center = true;
+            } else {
+                return error();
+            }
+            continue;
+        }
+
+        tokens.reconsume_current_input_token();
+        break;
+    }
+
+    if (found_center) {
+        if (horizontal.has_value() && vertical.has_value())
+            return error();
+        if (!horizontal.has_value())
+            horizontal = EdgeOffset { PositionEdge::Left, center_offset, true, false };
+        if (!vertical.has_value())
+            vertical = EdgeOffset { PositionEdge::Top, center_offset, true, false };
+    }
+
+    if (!horizontal.has_value() && !vertical.has_value())
+        return error();
+
+    // Unpack `<edge> <length>`:
+    // The loop above reads this pattern as a single EdgeOffset, when actually, it should be treated
+    // as `x y` if the edge is horizontal, and `y` (with the second token reconsumed) otherwise.
+    if (!vertical.has_value() && horizontal->edge_provided && horizontal->offset_provided) {
+        // Split into `x y`
+        vertical = EdgeOffset { PositionEdge::Top, horizontal->offset, false, true };
+        horizontal->offset = zero_offset;
+        horizontal->offset_provided = false;
+    } else if (!horizontal.has_value() && vertical->edge_provided && vertical->offset_provided) {
+        // `y`, reconsume
+        vertical->offset = zero_offset;
+        vertical->offset_provided = false;
+        tokens.reconsume_current_input_token();
+    }
+
+    // If only one value is specified, the second value is assumed to be center.
+    if (!horizontal.has_value())
+        horizontal = EdgeOffset { PositionEdge::Left, center_offset, false, false };
+    if (!vertical.has_value())
+        vertical = EdgeOffset { PositionEdge::Top, center_offset, false, false };
+
+    return PositionStyleValue::create(
+        horizontal->edge, horizontal->offset,
+        vertical->edge, vertical->offset);
+}
+
+RefPtr<StyleValue> Parser::parse_background_position_value(ParsingContext const& context, Vector<StyleComponentValueRule> const& component_values)
+{
+    auto tokens = TokenStream { component_values };
+    // FIXME: Handle multiple sets of comma-separated values.
+    return parse_single_background_position_value(context, tokens);
+}
+
 RefPtr<StyleValue> Parser::parse_background_repeat_value(ParsingContext const& context, Vector<StyleComponentValueRule> const& component_values)
 {
     auto is_directional_repeat = [](StyleValue const& value) -> bool {
@@ -3245,6 +3414,10 @@ Result<NonnullRefPtr<StyleValue>, Parser::ParsingResult> Parser::parse_css_value
         if (auto parsed_value = parse_background_image_value(m_context, component_values))
             return parsed_value.release_nonnull();
         return ParsingResult::SyntaxError;
+    case PropertyID::BackgroundPosition:
+        if (auto parsed_value = parse_background_position_value(m_context, component_values))
+            return parsed_value.release_nonnull();
+        return ParsingResult::SyntaxError;
     case PropertyID::BackgroundRepeat:
         if (auto parsed_value = parse_background_repeat_value(m_context, component_values))
             return parsed_value.release_nonnull();

+ 2 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -214,6 +214,8 @@ private:
     static RefPtr<StyleValue> parse_image_value(ParsingContext const&, StyleComponentValueRule const&);
     static RefPtr<StyleValue> parse_background_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);
     static RefPtr<StyleValue> parse_background_image_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);
+    static RefPtr<StyleValue> parse_single_background_position_value(ParsingContext const&, TokenStream<StyleComponentValueRule>&);
+    static RefPtr<StyleValue> parse_background_position_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);
     static RefPtr<StyleValue> parse_background_repeat_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);
     static RefPtr<StyleValue> parse_border_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);
     static RefPtr<StyleValue> parse_border_radius_value(ParsingContext const&, Vector<StyleComponentValueRule> const&);

+ 15 - 0
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -343,6 +343,21 @@ static void set_property_expanding_shorthands(StyleProperties& style, CSS::Prope
         return;
     }
 
+    if (property_id == CSS::PropertyID::BackgroundPosition) {
+        if (value.is_value_list()) {
+            auto& background_position_list = value.as_value_list().values();
+            // FIXME: Handle multiple backgrounds.
+            if (!background_position_list.is_empty()) {
+                auto& background_position = background_position_list.first();
+                style.set_property(CSS::PropertyID::BackgroundPosition, background_position);
+            }
+            return;
+        }
+
+        style.set_property(CSS::PropertyID::BackgroundPosition, value);
+        return;
+    }
+
     if (property_id == CSS::PropertyID::BackgroundRepeat) {
         if (value.is_value_list()) {
             auto& background_repeat_list = value.as_value_list().values();

+ 25 - 0
Userland/Libraries/LibWeb/CSS/StyleValue.cpp

@@ -139,6 +139,12 @@ OverflowStyleValue const& StyleValue::as_overflow() const
     return static_cast<OverflowStyleValue const&>(*this);
 }
 
+PositionStyleValue const& StyleValue::as_position() const
+{
+    VERIFY(is_position());
+    return static_cast<PositionStyleValue const&>(*this);
+}
+
 StringStyleValue const& StyleValue::as_string() const
 {
     VERIFY(is_string());
@@ -401,4 +407,23 @@ String ColorStyleValue::to_string() const
     return String::formatted("rgba({}, {}, {}, {})", m_color.red(), m_color.green(), m_color.blue(), (float)(m_color.alpha()) / 255.0f);
 }
 
+String PositionStyleValue::to_string() const
+{
+    auto to_string = [](PositionEdge edge) {
+        switch (edge) {
+        case PositionEdge::Left:
+            return "left";
+        case PositionEdge::Right:
+            return "right";
+        case PositionEdge::Top:
+            return "top";
+        case PositionEdge::Bottom:
+            return "bottom";
+        }
+        VERIFY_NOT_REACHED();
+    };
+
+    return String::formatted("{} {} {} {}", to_string(m_edge_x), m_offset_x.to_string(), to_string(m_edge_y), m_offset_y.to_string());
+}
+
 }

+ 44 - 2
Userland/Libraries/LibWeb/CSS/StyleValue.h

@@ -167,6 +167,13 @@ enum class Position {
     Sticky,
 };
 
+enum class PositionEdge {
+    Left,
+    Right,
+    Top,
+    Bottom,
+};
+
 enum class Repeat : u8 {
     NoRepeat,
     Repeat,
@@ -242,6 +249,7 @@ public:
         ListStyle,
         Numeric,
         Overflow,
+        Position,
         String,
         TextDecoration,
         Transformation,
@@ -270,6 +278,7 @@ public:
     bool is_list_style() const { return type() == Type::ListStyle; }
     bool is_numeric() const { return type() == Type::Numeric; }
     bool is_overflow() const { return type() == Type::Overflow; }
+    bool is_position() const { return type() == Type::Position; }
     bool is_string() const { return type() == Type::String; }
     bool is_text_decoration() const { return type() == Type::TextDecoration; }
     bool is_transformation() const { return type() == Type::Transformation; }
@@ -278,8 +287,8 @@ public:
 
     bool is_builtin() const { return is_inherit() || is_initial() || is_unset(); }
 
-    BackgroundRepeatStyleValue const& as_background_repeat() const;
     BackgroundStyleValue const& as_background() const;
+    BackgroundRepeatStyleValue const& as_background_repeat() const;
     BorderRadiusStyleValue const& as_border_radius() const;
     BorderStyleValue const& as_border() const;
     BoxShadowStyleValue const& as_box_shadow() const;
@@ -297,14 +306,15 @@ public:
     ListStyleStyleValue const& as_list_style() const;
     NumericStyleValue const& as_numeric() const;
     OverflowStyleValue const& as_overflow() const;
+    PositionStyleValue const& as_position() const;
     StringStyleValue const& as_string() const;
     TextDecorationStyleValue const& as_text_decoration() const;
     TransformationStyleValue const& as_transformation() const;
     UnsetStyleValue const& as_unset() const;
     StyleValueList const& as_value_list() const;
 
-    BackgroundRepeatStyleValue& as_background_repeat() { return const_cast<BackgroundRepeatStyleValue&>(const_cast<StyleValue const&>(*this).as_background_repeat()); }
     BackgroundStyleValue& as_background() { return const_cast<BackgroundStyleValue&>(const_cast<StyleValue const&>(*this).as_background()); }
+    BackgroundRepeatStyleValue& as_background_repeat() { return const_cast<BackgroundRepeatStyleValue&>(const_cast<StyleValue const&>(*this).as_background_repeat()); }
     BorderRadiusStyleValue& as_border_radius() { return const_cast<BorderRadiusStyleValue&>(const_cast<StyleValue const&>(*this).as_border_radius()); }
     BorderStyleValue& as_border() { return const_cast<BorderStyleValue&>(const_cast<StyleValue const&>(*this).as_border()); }
     BoxShadowStyleValue& as_box_shadow() { return const_cast<BoxShadowStyleValue&>(const_cast<StyleValue const&>(*this).as_box_shadow()); }
@@ -322,6 +332,7 @@ public:
     ListStyleStyleValue& as_list_style() { return const_cast<ListStyleStyleValue&>(const_cast<StyleValue const&>(*this).as_list_style()); }
     NumericStyleValue& as_numeric() { return const_cast<NumericStyleValue&>(const_cast<StyleValue const&>(*this).as_numeric()); }
     OverflowStyleValue& as_overflow() { return const_cast<OverflowStyleValue&>(const_cast<StyleValue const&>(*this).as_overflow()); }
+    PositionStyleValue& as_position() { return const_cast<PositionStyleValue&>(const_cast<StyleValue const&>(*this).as_position()); }
     StringStyleValue& as_string() { return const_cast<StringStyleValue&>(const_cast<StyleValue const&>(*this).as_string()); }
     TextDecorationStyleValue& as_text_decoration() { return const_cast<TextDecorationStyleValue&>(const_cast<StyleValue const&>(*this).as_text_decoration()); }
     TransformationStyleValue& as_transformation() { return const_cast<TransformationStyleValue&>(const_cast<StyleValue const&>(*this).as_transformation()); }
@@ -407,6 +418,37 @@ private:
     // FIXME: background-origin
 };
 
+class PositionStyleValue final : public StyleValue {
+public:
+    static NonnullRefPtr<PositionStyleValue> create(PositionEdge edge_x, Length const& offset_x, PositionEdge edge_y, Length const& offset_y)
+    {
+        return adopt_ref(*new PositionStyleValue(edge_x, offset_x, edge_y, offset_y));
+    }
+    virtual ~PositionStyleValue() override { }
+
+    PositionEdge edge_x() const { return m_edge_x; }
+    Length const& offset_x() const { return m_offset_x; }
+    PositionEdge edge_y() const { return m_edge_y; }
+    Length const& offset_y() const { return m_offset_y; }
+
+    virtual String to_string() const override;
+
+private:
+    PositionStyleValue(PositionEdge edge_x, Length const& offset_x, PositionEdge edge_y, Length const& offset_y)
+        : StyleValue(Type::Position)
+        , m_edge_x(edge_x)
+        , m_offset_x(offset_x)
+        , m_edge_y(edge_y)
+        , m_offset_y(offset_y)
+    {
+    }
+
+    PositionEdge m_edge_x;
+    Length m_offset_x;
+    PositionEdge m_edge_y;
+    Length m_offset_y;
+};
+
 class BackgroundRepeatStyleValue final : public StyleValue {
 public:
     static NonnullRefPtr<BackgroundRepeatStyleValue> create(NonnullRefPtr<StyleValue> repeat_x, NonnullRefPtr<StyleValue> repeat_y)

+ 1 - 0
Userland/Libraries/LibWeb/Forward.h

@@ -53,6 +53,7 @@ class MediaQueryList;
 class MediaQueryListEvent;
 class NumericStyleValue;
 class OverflowStyleValue;
+class PositionStyleValue;
 class PropertyOwningCSSStyleDeclaration;
 class Screen;
 class Selector;