srctree

Robin Linden parent 944d95cb d2d4a20a
css: Expand the border and border-<side> shorthand properties

I had to move is_in_array a bit higher up in the parser as a workaroundfor Clang 14 and earlier.

inlinesplit
css/parser.h added: 171, removed: 8, total 163
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2022 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021-2023 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021 Mikael Larsson <c.mikael.larsson@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
@@ -18,6 +18,7 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <string_view>
@@ -102,6 +103,16 @@ private:
}
}
 
static constexpr std::array border_shorthand_properties{
"border", "border-left", "border-right", "border-top", "border-bottom"};
 
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-style
static constexpr std::array border_style_keywords{
"none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"};
 
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-width
static constexpr std::array border_width_keywords{"thin", "medium", "thick"};
 
static constexpr auto shorthand_edge_property = std::array{"padding", "margin", "border-style"};
 
static constexpr auto absolute_size_keywords =
@@ -122,6 +133,11 @@ private:
 
static constexpr std::string_view dot_and_digits = ".0123456789";
 
template<auto const &array>
constexpr bool is_in_array(std::string_view str) const {
return std::ranges::find(array, str) != std::cend(array);
}
 
class Tokenizer {
public:
Tokenizer(std::string_view str, char delimiter) {
@@ -218,11 +234,99 @@ private:
expand_font(declarations, value);
} else if (name == "border-radius") {
expand_border_radius_values(declarations, value);
} else if (is_in_array<border_shorthand_properties>(name)) {
expand_border(name, declarations, value);
} else {
declarations.insert_or_assign(property_id_from_string(name), std::string{value});
}
}
 
enum class BorderSide { Left, Right, Top, Bottom };
 
// https://developer.mozilla.org/en-US/docs/Web/CSS/border
void expand_border(
std::string_view name, std::map<PropertyId, std::string> &declarations, std::string_view value) const {
if (name == "border") {
expand_border_impl(BorderSide::Left, declarations, value);
expand_border_impl(BorderSide::Right, declarations, value);
expand_border_impl(BorderSide::Top, declarations, value);
expand_border_impl(BorderSide::Bottom, declarations, value);
} else if (name == "border-left") {
expand_border_impl(BorderSide::Left, declarations, value);
} else if (name == "border-right") {
expand_border_impl(BorderSide::Right, declarations, value);
} else if (name == "border-top") {
expand_border_impl(BorderSide::Top, declarations, value);
} else if (name == "border-bottom") {
expand_border_impl(BorderSide::Bottom, declarations, value);
}
}
 
void expand_border_impl(
BorderSide side, std::map<PropertyId, std::string> &declarations, std::string_view value) const {
auto [color_id, style_id, width_id] = [&] {
switch (side) {
case BorderSide::Left:
return std::tuple{
PropertyId::BorderLeftColor, PropertyId::BorderLeftStyle, PropertyId::BorderLeftWidth};
case BorderSide::Right:
return std::tuple{
PropertyId::BorderRightColor, PropertyId::BorderRightStyle, PropertyId::BorderRightWidth};
case BorderSide::Top:
return std::tuple{
PropertyId::BorderTopColor, PropertyId::BorderTopStyle, PropertyId::BorderTopWidth};
case BorderSide::Bottom:
return std::tuple{PropertyId::BorderBottomColor,
PropertyId::BorderBottomStyle,
PropertyId::BorderBottomWidth};
}
std::abort(); // Unreachable.
}();
 
Tokenizer tokenizer(value, ' ');
if (tokenizer.size() == 0 || tokenizer.size() > 3) {
// TODO(robinlinden): Propagate info about invalid properties.
return;
}
 
enum class BorderPropertyType { Color, Style, Width };
auto guess_type = [this](std::string_view v) -> BorderPropertyType {
if (is_in_array<border_style_keywords>(v)) {
return BorderPropertyType::Style;
}
 
if (v.find_first_of("0123456789") == 0 || is_in_array<border_width_keywords>(v)) {
return BorderPropertyType::Width;
}
 
return BorderPropertyType::Color;
};
 
std::optional<std::string_view> color;
std::optional<std::string_view> style;
std::optional<std::string_view> width;
 
// TODO(robinlinden): Duplicate color/style/width shouldn't be
// tolerated, but we have no way of propagating that info right now.
for (auto v = tokenizer.get(); v.has_value(); v = tokenizer.next().get()) {
switch (guess_type(*v)) {
case BorderPropertyType::Color:
color = *v;
break;
case BorderPropertyType::Style:
style = *v;
break;
case BorderPropertyType::Width:
width = *v;
break;
}
}
 
declarations.insert_or_assign(color_id, color.value_or("currentcolor"));
declarations.insert_or_assign(style_id, style.value_or("none"));
declarations.insert_or_assign(width_id, width.value_or("medium"));
}
 
// https://developer.mozilla.org/en-US/docs/Web/CSS/background
// TODO(robinlinden): This only handles a color being named, and assumes any single item listed is a color.
static void expand_background(std::map<PropertyId, std::string> &declarations, std::string_view value) {
@@ -463,11 +567,6 @@ private:
return result;
}
 
template<auto const &array>
constexpr bool is_in_array(std::string_view str) const {
return std::ranges::find(array, str) != std::cend(array);
}
 
constexpr bool is_shorthand_edge_property(std::string_view str) const {
return is_in_array<shorthand_edge_property>(str);
}
 
css/parser_test.cpp added: 171, removed: 8, total 163
@@ -833,5 +833,69 @@ int main() {
expect(src.contains(R"(url("/fonts/OpenSans-Regular-webfont.woff") format("woff")"));
});
 
etest::test("parser: border shorthand, all values", [] {
auto rules = css::parse("p { border: 5px black solid; }");
require(rules.size() == 1);
auto const &p = rules[0];
expect_eq(p.declarations,
std::map<css::PropertyId, std::string>{
{css::PropertyId::BorderBottomColor, "black"s},
{css::PropertyId::BorderBottomStyle, "solid"s},
{css::PropertyId::BorderBottomWidth, "5px"s},
{css::PropertyId::BorderLeftColor, "black"s},
{css::PropertyId::BorderLeftStyle, "solid"s},
{css::PropertyId::BorderLeftWidth, "5px"s},
{css::PropertyId::BorderRightColor, "black"s},
{css::PropertyId::BorderRightStyle, "solid"s},
{css::PropertyId::BorderRightWidth, "5px"s},
{css::PropertyId::BorderTopColor, "black"s},
{css::PropertyId::BorderTopStyle, "solid"s},
{css::PropertyId::BorderTopWidth, "5px"s},
});
});
 
etest::test("parser: border shorthand, color+style", [] {
auto rules = css::parse("p { border-bottom: #123 dotted; }");
require(rules.size() == 1);
auto const &p = rules[0];
expect_eq(p.declarations,
std::map<css::PropertyId, std::string>{
{css::PropertyId::BorderBottomColor, "#123"s},
{css::PropertyId::BorderBottomStyle, "dotted"s},
{css::PropertyId::BorderBottomWidth, "medium"s},
});
});
 
etest::test("parser: border shorthand, width+style", [] {
auto rules = css::parse("p { border-left: ridge 30em; }");
require(rules.size() == 1);
auto const &p = rules[0];
expect_eq(p.declarations,
std::map<css::PropertyId, std::string>{
{css::PropertyId::BorderLeftColor, "currentcolor"s},
{css::PropertyId::BorderLeftStyle, "ridge"s},
{css::PropertyId::BorderLeftWidth, "30em"s},
});
});
 
etest::test("parser: border shorthand, width", [] {
auto rules = css::parse("p { border-right: thin; }");
require(rules.size() == 1);
auto const &p = rules[0];
expect_eq(p.declarations,
std::map<css::PropertyId, std::string>{
{css::PropertyId::BorderRightColor, "currentcolor"s},
{css::PropertyId::BorderRightStyle, "none"s},
{css::PropertyId::BorderRightWidth, "thin"s},
});
});
 
etest::test("parser: border shorthand, too many values", [] {
auto rules = css::parse("p { border-top: outset #123 none solid; }");
require(rules.size() == 1);
auto const &p = rules[0];
expect_eq(p.declarations, std::map<css::PropertyId, std::string>{});
});
 
return etest::run_all_tests();
}