diff options
| author | George Hazan <george.hazan@gmail.com> | 2024-10-09 18:13:40 +0300 |
|---|---|---|
| committer | George Hazan <george.hazan@gmail.com> | 2024-10-09 18:13:40 +0300 |
| commit | 0e86b853be3b5f809ed1decbf636221c1144a386 (patch) | |
| tree | 4ce84d9849646a559d5afece33fc6e64d39e4a50 /libs/litehtml/src/style.cpp | |
| parent | 834cbb58d74215980165eab257538ba918a378cd (diff) | |
litehtml update
Diffstat (limited to 'libs/litehtml/src/style.cpp')
| -rw-r--r-- | libs/litehtml/src/style.cpp | 1907 |
1 files changed, 1076 insertions, 831 deletions
diff --git a/libs/litehtml/src/style.cpp b/libs/litehtml/src/style.cpp index 05d366d867..10768700a6 100644 --- a/libs/litehtml/src/style.cpp +++ b/libs/litehtml/src/style.cpp @@ -1,9 +1,25 @@ #include "html.h" #include "style.h" +#include "css_parser.h" +#include "internal.h" + + +// All functions here assume that whitespace have been removed. namespace litehtml { +bool parse_bg_image(const css_token& token, image& bg_image, document_container* container); +bool parse_bg_position_size(const css_token_vector& tokens, int& index, css_length& x, css_length& y, css_size& size); +bool parse_bg_size(const css_token_vector& tokens, int& index, css_size& size); +bool parse_two_lengths(const css_token_vector& tokens, css_length len[2], int options); +template<class T, class... Args> +int parse_1234_values(const css_token_vector& tokens, T result[4], bool (*func)(const css_token&, T&, Args...), Args... args); +int parse_1234_lengths(const css_token_vector& tokens, css_length len[4], int options, string keywords = ""); +bool parse_border_width(const css_token& tok, css_length& width); +bool parse_font_family(const css_token_vector& tokens, string& font_family); +bool parse_font_weight(const css_token& tok, css_length& weight); + std::map<string_id, string> style::m_valid_values = { { _display_, style_display_strings }, @@ -41,91 +57,137 @@ std::map<string_id, string> style::m_valid_values = { _flex_direction_, flex_direction_strings }, { _flex_wrap_, flex_wrap_strings }, { _justify_content_, flex_justify_content_strings }, - { _align_items_, flex_align_items_strings }, { _align_content_, flex_align_content_strings }, + { _align_items_, flex_align_items_strings }, { _align_self_, flex_align_items_strings }, { _caption_side_, caption_side_strings }, }; -void style::parse(const string& txt, const string& baseurl, document_container* container) +std::map<string_id, vector<string_id>> shorthands = { - std::vector<string> properties; - split_string(txt, properties, ";", "", "\"'"); + { _font_, {_font_style_, _font_variant_, _font_weight_, _font_size_, _line_height_, _font_family_}}, + + { _background_, { + _background_color_, + _background_position_x_, + _background_position_y_, + _background_repeat_, + _background_attachment_, + _background_image_, + _background_image_baseurl_, + _background_size_, + _background_origin_, + _background_clip_ + } }, + + { _list_style_, {_list_style_image_, _list_style_image_baseurl_, _list_style_position_, _list_style_type_}}, + + { _margin_, {_margin_top_, _margin_right_, _margin_bottom_, _margin_left_}}, + { _padding_, {_padding_top_, _padding_right_, _padding_bottom_, _padding_left_}}, + + { _border_width_, {_border_top_width_, _border_right_width_, _border_bottom_width_, _border_left_width_}}, + { _border_style_, {_border_top_style_, _border_right_style_, _border_bottom_style_, _border_left_style_}}, + { _border_color_, {_border_top_color_, _border_right_color_, _border_bottom_color_, _border_left_color_}}, + { _border_top_, {_border_top_width_, _border_top_style_, _border_top_color_}}, + { _border_right_, {_border_right_width_, _border_right_style_, _border_right_color_}}, + { _border_bottom_, {_border_bottom_width_, _border_bottom_style_, _border_bottom_color_}}, + { _border_left_, {_border_left_width_, _border_left_style_, _border_left_color_}}, + { _border_, { + _border_top_width_, _border_right_width_, _border_bottom_width_, _border_left_width_, + _border_top_style_, _border_right_style_, _border_bottom_style_, _border_left_style_, + _border_top_color_, _border_right_color_, _border_bottom_color_, _border_left_color_ + } }, + + { _flex_, {_flex_grow_, _flex_shrink_, _flex_basis_}}, + { _flex_flow_, {_flex_direction_, _flex_wrap_}}, +}; - for(const auto & property : properties) +void style::add(const string& txt, const string& baseurl, document_container* container) +{ + auto tokens = normalize(txt, f_componentize); + add(tokens, baseurl, container); +} + +void style::add(const css_token_vector& tokens, const string& baseurl, document_container* container) +{ + raw_declaration::vector decls; + raw_rule::vector rules; + css_parser(tokens).consume_style_block_contents(decls, rules); + if (!rules.empty()) + css_parse_error("rule inside a style block"); + if (decls.empty()) + return; + + // Parse each declaration + for (auto& decl : decls) { - parse_property(property, baseurl, container); + remove_whitespace(decl.value); + // Note: decl.value is already componentized, see consume_qualified_rule and consume_style_block_contents. + // Note: decl.value may be empty. + string name = decl.name.substr(0, 2) == "--" ? decl.name : lowcase(decl.name); + add_property(_id(name), decl.value, baseurl, decl.important, container); } } -void style::parse_property(const string& txt, const string& baseurl, document_container* container) +bool has_var(const css_token_vector& tokens) { - string::size_type pos = txt.find_first_of(':'); - if(pos != string::npos) + for (auto& tok : tokens) { - string name = txt.substr(0, pos); - string val = txt.substr(pos + 1); - - trim(name); lcase(name); - trim(val); - - if(!name.empty() && !val.empty()) - { - string_vector vals; - split_string(val, vals, "!"); - if(vals.size() == 1) - { - add_property(_id(name), val, baseurl, false, container); - } else if(vals.size() > 1) - { - trim(vals[0]); - lcase(vals[1]); - add_property(_id(name), vals[0], baseurl, vals[1] == "important", container); - } - } + if (tok.type == CV_FUNCTION && lowcase(tok.name) == "var") + return true; + if (tok.is_component_value() && has_var(tok.value)) + return true; } + return false; } void style::inherit_property(string_id name, bool important) { - switch (name) + auto atomic_properties = at(shorthands, name); + if (!atomic_properties.empty()) { - case _font_: - add_parsed_property(_font_style_, property_value(inherit(), important)); - add_parsed_property(_font_variant_, property_value(inherit(), important)); - add_parsed_property(_font_weight_, property_value(inherit(), important)); - add_parsed_property(_font_size_, property_value(inherit(), important)); - add_parsed_property(_line_height_, property_value(inherit(), important)); - break; - case _background_: - add_parsed_property(_background_color_, property_value(inherit(), important)); - add_parsed_property(_background_position_x_, property_value(inherit(), important)); - add_parsed_property(_background_position_y_, property_value(inherit(), important)); - add_parsed_property(_background_repeat_, property_value(inherit(), important)); - add_parsed_property(_background_attachment_, property_value(inherit(), important)); - add_parsed_property(_background_image_, property_value(inherit(), important)); - add_parsed_property(_background_image_baseurl_, property_value(inherit(), important)); - add_parsed_property(_background_size_, property_value(inherit(), important)); - add_parsed_property(_background_origin_, property_value(inherit(), important)); - add_parsed_property(_background_clip_, property_value(inherit(), important)); - break; - default: - add_parsed_property(name, property_value(inherit(), important)); + for (auto atomic : atomic_properties) + add_parsed_property(atomic, property_value(inherit(), important)); } + else + add_parsed_property(name, property_value(inherit(), important)); } -void style::add_property(string_id name, const string& val, const string& baseurl, bool important, document_container* container) +void style::add_length_property(string_id name, css_token val, string keywords, int options, bool important) { - if (val.find("var(") != string::npos) return add_parsed_property(name, property_value(val, important, true)); - if (val == "inherit") return inherit_property(name, important); + css_length length; + if (length.from_token(val, options, keywords)) + add_parsed_property(name, property_value(length, important)); +} - string url; - css_length len[4], length; +// `value` is a list of component values with all whitespace tokens removed, including those inside component values +void style::add_property(string_id name, const css_token_vector& value, const string& baseurl, bool important, document_container* container) +{ + // Note: empty value is a valid value for a custom property. + if (value.empty() && _s(name).substr(0, 2) != "--") + return; + + if (has_var(value)) + return add_parsed_property(name, property_value(value, important, true)); + + // valid only if value contains a single token + css_token val = value.size() == 1 ? value[0] : css_token(); + // nonempty if value is a single identifier + string ident = val.ident(); + + if (ident == "inherit") + return inherit_property(name, important); + + int idx[4]; + web_color clr[4]; + css_length len[4]; + string str; switch (name) { - // keyword-only properties + // ============================= SINGLE KEYWORD ============================= + case _display_: case _visibility_: case _position_: @@ -141,7 +203,6 @@ void style::add_property(string_id name, const string& val, const string& baseur case _font_style_: case _font_variant_: - case _font_weight_: case _list_style_type_: case _list_style_position_: @@ -158,1036 +219,1174 @@ void style::add_property(string_id name, const string& val, const string& baseur case _align_content_: case _caption_side_: - { - int idx = value_index(val, m_valid_values[name]); - if (idx >= 0) - { - add_parsed_property(name, property_value(idx, important)); - } - break; - } - case _align_items_: - case _align_self_: - parse_align_self(name, val, important); + + if (int index = value_index(ident, m_valid_values[name]); index >= 0) + add_parsed_property(name, property_value(index, important)); break; - // <length> + // ============================= LENGTH ============================= + + // auto | <integer> https://developer.mozilla.org/en-US/docs/Web/CSS/z-index#formal_syntax + case _z_index_: + return add_length_property(name, val, "auto", f_integer, important); + + // <length-percentage> https://developer.mozilla.org/en-US/docs/Web/CSS/text-indent#formal_syntax case _text_indent_: + return add_length_property(name, val, "", f_length_percentage, important); + + // <length-percentage [0,∞]> https://developer.mozilla.org/en-US/docs/Web/CSS/padding-left case _padding_left_: case _padding_right_: case _padding_top_: case _padding_bottom_: - length.fromString(val); - add_parsed_property(name, property_value(length, important)); - break; - - // <length> | auto + return add_length_property(name, val, "", f_length_percentage|f_positive, important); + + // auto | <length-percentage> https://developer.mozilla.org/en-US/docs/Web/CSS/left case _left_: case _right_: case _top_: case _bottom_: - case _z_index_: // <integer> | auto - case _width_: - case _height_: - case _min_width_: - case _min_height_: case _margin_left_: case _margin_right_: case _margin_top_: case _margin_bottom_: - length.fromString(val, "auto", -1); - add_parsed_property(name, property_value(length, important)); - break; + return add_length_property(name, val, "auto", f_length_percentage, important); + + // auto | min-content | max-content | fit-content | <length-percentage [0,∞]> https://developer.mozilla.org/en-US/docs/Web/CSS/width#formal_syntax + case _width_: + case _height_: + case _min_width_: + case _min_height_: + return add_length_property(name, val, "auto", f_length_percentage|f_positive, important); - // <length> | none + // none | min-content | max-content | fit-content | <length-percentage [0,∞]> https://developer.mozilla.org/en-US/docs/Web/CSS/max-width#formal_syntax case _max_width_: case _max_height_: - length.fromString(val, "none", -1); - add_parsed_property(name, property_value(length, important)); - break; - + return add_length_property(name, val, "none", f_length_percentage|f_positive, important); + + // normal | <number [0,∞]> | <length-percentage [0,∞]> https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#formal_syntax case _line_height_: - length.fromString(val, "normal", -1); - add_parsed_property(name, property_value(length, important)); - break; + return add_length_property(name, val, "normal", f_number|f_length_percentage|f_positive, important); + // font-size = <absolute-size> | <relative-size> | <length-percentage [0,∞]> https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#formal_syntax case _font_size_: - length.fromString(val, font_size_strings, -1); - add_parsed_property(name, property_value(length, important)); + return add_length_property(name, val, font_size_strings, f_length_percentage|f_positive, important); + + case _margin_: + if (int n = parse_1234_lengths(value, len, f_length_percentage, "auto")) + add_four_properties(_margin_top_, len, n, important); break; - // Parse background shorthand properties - case _background_: - parse_background(val, baseurl, important, container); + case _padding_: + if (int n = parse_1234_lengths(value, len, f_length_percentage | f_positive)) + add_four_properties(_padding_top_, len, n, important); break; + + // ============================= COLOR ============================= - case _background_image_: - parse_background_image(val, container, baseurl, important); + case _color_: + if (ident == "currentcolor") return inherit_property(name, important); + // fallthrough + case _background_color_: + case _border_top_color_: + case _border_bottom_color_: + case _border_left_color_: + case _border_right_color_: + if (parse_color(val, *clr, container)) + add_parsed_property(name, property_value(*clr, important)); break; - case _background_attachment_: - case _background_repeat_: - case _background_clip_: - case _background_origin_: - parse_keyword_comma_list(name, val, important); + // ============================= BACKGROUND ============================= + + case _background_: + parse_background(value, baseurl, important, container); + break; + + case _background_image_: + parse_background_image(value, baseurl, important, container); break; case _background_position_: - parse_background_position(val, important); + parse_background_position(value, important); break; case _background_size_: - parse_background_size(val, important); + parse_background_size(value, important); break; - // Parse border spacing properties - case _border_spacing_: - parse_two_lengths(val, len); - add_parsed_property(__litehtml_border_spacing_x_, property_value(len[0], important)); - add_parsed_property(__litehtml_border_spacing_y_, property_value(len[1], important)); + case _background_repeat_: + case _background_attachment_: + case _background_origin_: + case _background_clip_: + parse_keyword_comma_list(name, value, important); break; - // Parse borders shorthand properties + // ============================= BORDER ============================= + case _border_: - { - string_vector tokens; - split_string(val, tokens, " ", "", "("); - for (const auto& token : tokens) - { - int idx = value_index(token, border_style_strings); - if (idx >= 0) - { - property_value style(idx, important); - add_parsed_property(_border_left_style_, style); - add_parsed_property(_border_right_style_, style); - add_parsed_property(_border_top_style_, style); - add_parsed_property(_border_bottom_style_, style); - } - else if (t_isdigit(token[0]) || token[0] == '.' || - value_in_list(token, border_width_strings)) - { - property_value width(parse_border_width(token), important); - add_parsed_property(_border_left_width_, width); - add_parsed_property(_border_right_width_, width); - add_parsed_property(_border_top_width_, width); - add_parsed_property(_border_bottom_width_, width); - } - else if (web_color::is_color(token, container)) - { - web_color _color = web_color::from_string(token, container); - property_value color(_color, important); - add_parsed_property(_border_left_color_, color); - add_parsed_property(_border_right_color_, color); - add_parsed_property(_border_top_color_, color); - add_parsed_property(_border_bottom_color_, color); - } - } + parse_border(value, important, container); break; - } case _border_left_: case _border_right_: case _border_top_: case _border_bottom_: - { - string_vector tokens; - split_string(val, tokens, " ", "", "("); - for (const auto& token : tokens) - { - int idx = value_index(token, border_style_strings); - if (idx >= 0) - { - add_parsed_property(_id(_s(name) + "-style"), property_value(idx, important)); - } - else if (t_isdigit(token[0]) || token[0] == '.' || - value_in_list(token, border_width_strings)) - { - property_value width(parse_border_width(token), important); - add_parsed_property(_id(_s(name) + "-width"), width); - } - else if (web_color::is_color(token, container)) - { - web_color color = web_color::from_string(token, container); - add_parsed_property(_id(_s(name) + "-color"), property_value(color, important)); - } - } + parse_border_side(name, value, important, container); break; - } - // Parse border-width/style/color shorthand properties case _border_width_: + if (int n = parse_1234_values(value, len, parse_border_width)) + add_four_properties(_border_top_width_, len, n, important); + break; case _border_style_: + if (int n = parse_1234_values(value, idx, parse_keyword, (string)border_style_strings, 0)) + add_four_properties(_border_top_style_, idx, n, important); + break; case _border_color_: - { - string prop = name == _border_width_ ? "-width" : name == _border_style_ ? "-style" : "-color"; - - string_vector tokens; - split_string(val, tokens, " "); - if (tokens.size() == 4) - { - add_property(_id("border-top" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-right" + prop), tokens[1], baseurl, important, container); - add_property(_id("border-bottom" + prop), tokens[2], baseurl, important, container); - add_property(_id("border-left" + prop), tokens[3], baseurl, important, container); - } - else if (tokens.size() == 3) - { - add_property(_id("border-top" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-right" + prop), tokens[1], baseurl, important, container); - add_property(_id("border-left" + prop), tokens[1], baseurl, important, container); - add_property(_id("border-bottom" + prop), tokens[2], baseurl, important, container); - } - else if (tokens.size() == 2) - { - add_property(_id("border-top" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-bottom" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-right" + prop), tokens[1], baseurl, important, container); - add_property(_id("border-left" + prop), tokens[1], baseurl, important, container); - } - else if (tokens.size() == 1) - { - add_property(_id("border-top" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-bottom" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-right" + prop), tokens[0], baseurl, important, container); - add_property(_id("border-left" + prop), tokens[0], baseurl, important, container); - } + if (int n = parse_1234_values(value, clr, parse_color, container)) + add_four_properties(_border_top_color_, clr, n, important); break; - } case _border_top_width_: case _border_bottom_width_: case _border_left_width_: case _border_right_width_: - length = parse_border_width(val); - add_parsed_property(name, property_value(length, important)); + if (parse_border_width(val, *len)) + add_parsed_property(name, property_value(*len, important)); break; - case _color_: - case _background_color_: - case _border_top_color_: - case _border_bottom_color_: - case _border_left_color_: - case _border_right_color_: - if (web_color::is_color(val, container)) - { - web_color color = web_color::from_string(val, container); - add_parsed_property(name, property_value(color, important)); - } - break; - - // Parse border radius shorthand properties + // border-bottom-left-radius = <length-percentage [0,∞]>{1,2} https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom-left-radius case _border_bottom_left_radius_: case _border_bottom_right_radius_: case _border_top_right_radius_: case _border_top_left_radius_: - parse_two_lengths(val, len); - add_parsed_property(_id(_s(name) + "-x"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-y"), property_value(len[1], important)); - break; - - // Parse border-radius shorthand properties - case _border_radius_: - { - string_vector tokens; - split_string(val, tokens, "/"); - if (tokens.size() == 1) + if (parse_two_lengths(value, len, f_length_percentage | f_positive)) { - add_property(_border_radius_x_, tokens[0], baseurl, important, container); - add_property(_border_radius_y_, tokens[0], baseurl, important, container); - } - else if (tokens.size() >= 2) - { - add_property(_border_radius_x_, tokens[0], baseurl, important, container); - add_property(_border_radius_y_, tokens[1], baseurl, important, container); + add_parsed_property(_id(_s(name) + "-x"), property_value(len[0], important)); + add_parsed_property(_id(_s(name) + "-y"), property_value(len[1], important)); } break; - } + case _border_radius_x_: case _border_radius_y_: { - string_id top_left, top_right, bottom_right, bottom_left; - if (name == _border_radius_x_) - { - top_left = _border_top_left_radius_x_; - top_right = _border_top_right_radius_x_; - bottom_right = _border_bottom_right_radius_x_; - bottom_left = _border_bottom_left_radius_x_; - } - else - { - top_left = _border_top_left_radius_y_; - top_right = _border_top_right_radius_y_; - bottom_right = _border_bottom_right_radius_y_; - bottom_left = _border_bottom_left_radius_y_; - } + string_id top_left = name == _border_radius_x_ ? + _border_top_left_radius_x_ : + _border_top_left_radius_y_; - switch (parse_four_lengths(val, len)) - { - case 1: - add_parsed_property(top_left, property_value(len[0], important)); - add_parsed_property(top_right, property_value(len[0], important)); - add_parsed_property(bottom_right, property_value(len[0], important)); - add_parsed_property(bottom_left, property_value(len[0], important)); - break; - case 2: - add_parsed_property(top_left, property_value(len[0], important)); - add_parsed_property(top_right, property_value(len[1], important)); - add_parsed_property(bottom_right, property_value(len[0], important)); - add_parsed_property(bottom_left, property_value(len[1], important)); - break; - case 3: - add_parsed_property(top_left, property_value(len[0], important)); - add_parsed_property(top_right, property_value(len[1], important)); - add_parsed_property(bottom_right, property_value(len[2], important)); - add_parsed_property(bottom_left, property_value(len[1], important)); - break; - case 4: - add_parsed_property(top_left, property_value(len[0], important)); - add_parsed_property(top_right, property_value(len[1], important)); - add_parsed_property(bottom_right, property_value(len[2], important)); - add_parsed_property(bottom_left, property_value(len[3], important)); - break; - } + if (int n = parse_1234_lengths(value, len, f_length_percentage | f_positive)) + add_four_properties(top_left, len, n, important); break; } - // Parse list-style shorthand properties - case _list_style_: - { - add_parsed_property(_list_style_type_, property_value(list_style_type_disc, important)); - add_parsed_property(_list_style_position_, property_value(list_style_position_outside, important)); - add_parsed_property(_list_style_image_, property_value("", important)); - add_parsed_property(_list_style_image_baseurl_, property_value("", important)); - - string_vector tokens; - split_string(val, tokens, " ", "", "("); - for (const auto& token : tokens) + case _border_radius_: + parse_border_radius(value, important); + break; + + // border-spacing = <length>{1,2} https://developer.mozilla.org/en-US/docs/Web/CSS/border-spacing + // Lengths may not be negative. https://drafts.csswg.org/css2/#separated-borders + case _border_spacing_: + if (parse_two_lengths(value, len, f_length | f_positive)) { - int idx = value_index(token, list_style_type_strings); - if (idx >= 0) - { - add_parsed_property(_list_style_type_, property_value(idx, important)); - } - else - { - idx = value_index(token, list_style_position_strings); - if (idx >= 0) - { - add_parsed_property(_list_style_position_, property_value(idx, important)); - } - else if (!strncmp(token.c_str(), "url", 3)) - { - css::parse_css_url(token, url); - add_parsed_property(_list_style_image_, property_value(url, important)); - add_parsed_property(_list_style_image_baseurl_, property_value(baseurl, important)); - } - } + add_parsed_property(__litehtml_border_spacing_x_, property_value(len[0], important)); + add_parsed_property(__litehtml_border_spacing_y_, property_value(len[1], important)); } break; - } - case _list_style_image_: - css::parse_css_url(val, url); - add_parsed_property(_list_style_image_, property_value(url, important)); - add_parsed_property(_list_style_image_baseurl_, property_value(baseurl, important)); - break; + // ============================= LIST ============================= - // Parse margin and padding shorthand properties - case _margin_: - case _padding_: - { - switch (parse_four_lengths(val, len)) + case _list_style_image_: + if (string url; parse_list_style_image(val, url)) { - case 4: - add_parsed_property(_id(_s(name) + "-top"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-right"), property_value(len[1], important)); - add_parsed_property(_id(_s(name) + "-bottom"), property_value(len[2], important)); - add_parsed_property(_id(_s(name) + "-left"), property_value(len[3], important)); - break; - case 3: - add_parsed_property(_id(_s(name) + "-top"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-right"), property_value(len[1], important)); - add_parsed_property(_id(_s(name) + "-left"), property_value(len[1], important)); - add_parsed_property(_id(_s(name) + "-bottom"), property_value(len[2], important)); - break; - case 2: - add_parsed_property(_id(_s(name) + "-top"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-bottom"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-right"), property_value(len[1], important)); - add_parsed_property(_id(_s(name) + "-left"), property_value(len[1], important)); - break; - case 1: - add_parsed_property(_id(_s(name) + "-top"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-bottom"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-right"), property_value(len[0], important)); - add_parsed_property(_id(_s(name) + "-left"), property_value(len[0], important)); - break; + add_parsed_property(_list_style_image_, property_value(url, important)); + add_parsed_property(_list_style_image_baseurl_, property_value(baseurl, important)); } break; - } - // Parse font shorthand properties + case _list_style_: + parse_list_style(value, baseurl, important); + break; + + // ============================= FONT ============================= + case _font_: - parse_font(val, important); + parse_font(value, important); break; - // Parse flex-flow shorthand properties - case _flex_flow_: - { - string_vector tokens; - split_string(val, tokens, " "); - for (const auto& tok : tokens) - { - int idx; - if ((idx = value_index(tok, flex_direction_strings)) >= 0) - { - add_parsed_property(_flex_direction_, property_value(idx, important)); - } - else if ((idx = value_index(tok, flex_wrap_strings)) >= 0) - { - add_parsed_property(_flex_wrap_, property_value(idx, important)); - } - } + case _font_family_: + if (parse_font_family(value, str)) + add_parsed_property(name, property_value(str, important)); break; - } - // Parse flex shorthand properties + case _font_weight_: + if (parse_font_weight(val, *len)) + add_parsed_property(name, property_value(*len, important)); + break; + + case _text_decoration_: + str = get_repr(value, 0, -1, true); + add_parsed_property(name, property_value(str, important)); + break; + + // ============================= FLEX ============================= + case _flex_: - parse_flex(val, important); + parse_flex(value, important); break; - case _flex_grow_: + case _flex_grow_: // <number [0,∞]> case _flex_shrink_: - add_parsed_property(name, property_value(t_strtof(val), important)); + if (val.type == NUMBER && val.n.number >= 0) + add_parsed_property(name, property_value(val.n.number, important)); break; case _flex_basis_: - length.fromString(val, flex_basis_strings, -1); - add_parsed_property(_flex_basis_, property_value(length, important)); + add_length_property(name, val, flex_basis_strings, f_length_percentage|f_positive, important); + break; + + case _flex_flow_: + parse_flex_flow(value, important); + break; + + case _align_items_: + case _align_self_: + parse_align_self(name, value, important); break; case _order_: // <integer> - { - char* end; - int int_val = (int) strtol(val.c_str(), &end, 10); - if(end[0] == '\0') - { - add_parsed_property(name, property_value(int_val, important)); - } - } + if (val.type == NUMBER && val.n.number_type == css_number_integer) + add_parsed_property(name, property_value((int)val.n.number, important)); break; + + // ============================= COUNTER, CONTENT ============================= + case _counter_increment_: case _counter_reset_: - { - string_vector tokens; - split_string(val, tokens, " "); - add_parsed_property(name, property_value(tokens, important)); + { + // TODO: parse it properly here + string_vector strings; + for (const auto& tok : value) strings.push_back(tok.get_repr(true)); + add_parsed_property(name, property_value(strings, important)); break; } + case _content_: + // TODO: parse it properly here + str = get_repr(value, 0, -1, true); + add_parsed_property(name, property_value(str, important)); + break; + + // ================================== OTHER ================================== + + case _cursor_: + str = get_repr(value, 0, -1, true); + add_parsed_property(name, property_value(str, important)); + break; + + // ============================= CUSTOM PROPERTY ============================= + + // https://drafts.csswg.org/css-variables-2/#defining-variables default: - add_parsed_property(name, property_value(val, important)); + if (_s(name).substr(0, 2) == "--" && _s(name).size() >= 3 && + (value.empty() || is_declaration_value(value))) + add_parsed_property(name, property_value(value, important)); } } -css_length style::parse_border_width(const string& str) +void style::add_property(string_id name, const string& value, const string& baseurl, bool important, document_container* container) { - css_length len; - if (t_isdigit(str[0]) || str[0] == '.') + auto tokens = normalize(value, f_componentize | f_remove_whitespace); + add_property(name, tokens, baseurl, important, container); +} + +// This should be the same as parse_bg_image, but list-style-image is currently a string (not an image). +bool style::parse_list_style_image(const css_token& tok, string& url) +{ + if (tok.ident() == "none") { - len.fromString(str); + url = ""; + return true; } - else + + return parse_url(tok, url); +} + +// https://drafts.csswg.org/css-lists/#list-style-property +// <'list-style-position'> || <'list-style-image'> || <'list-style-type'> +void style::parse_list_style(const css_token_vector& tokens, string baseurl, bool important) +{ + // initial values: https://developer.mozilla.org/en-US/docs/Web/CSS/list-style#formal_definition + int type = list_style_type_disc; + int position = list_style_position_outside; + string image = ""; // none + + bool type_found = false; + bool position_found = false; + bool image_found = false; + int none_count = 0; + + for (const auto& token : tokens) { - int idx = value_index(str, border_width_strings); - if (idx >= 0) - { - len.set_value(border_width_values[idx], css_units_px); + // "...none is a valid value for both list-style-image and list-style-type. To resolve this ambiguity, + // a value of none ... must be applied to whichever of the two properties aren’t otherwise set by the shorthand." + if (token.ident() == "none") { + none_count++; + continue; } + if (!type_found && parse_keyword(token, type, list_style_type_strings)) + type_found = true; + else if (!position_found && parse_keyword(token, position, list_style_position_strings)) + position_found = true; + else if (!image_found && parse_list_style_image(token, image)) + image_found = true; + else + return; // syntax error + } + + switch (none_count) + { + case 0: + break; + case 1: + if (type_found && image_found) return; + if (!type_found) type = list_style_type_none; + break; + case 2: + if (type_found || image_found) return; + type = list_style_type_none; + break; + default: + return; // syntax error } - return len; + + add_parsed_property(_list_style_type_, property_value(type, important)); + add_parsed_property(_list_style_position_, property_value(position, important)); + add_parsed_property(_list_style_image_, property_value(image, important)); + add_parsed_property(_list_style_image_baseurl_, property_value(baseurl, important)); } -void style::parse_two_lengths(const string& str, css_length len[2]) +// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius +// border-radius = <length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]? +void style::parse_border_radius(const css_token_vector& tokens, bool important) { - string_vector tokens; - split_string(str, tokens, " "); - if (tokens.size() == 1) + int i; + for (i = 0; i < (int)tokens.size() && tokens[i].ch != '/'; i++) {} + + if (i == (int)tokens.size()) // no '/' { - css_length length; - length.fromString(tokens[0]); - len[0] = len[1] = length; + css_length len[4]; + if (int n = parse_1234_lengths(tokens, len, f_length_percentage | f_positive)) + { + add_four_properties(_border_top_left_radius_x_, len, n, important); + add_four_properties(_border_top_left_radius_y_, len, n, important); + } } - else if (tokens.size() == 2) + else { - len[0].fromString(tokens[0]); - len[1].fromString(tokens[1]); + auto raduis_x = slice(tokens, 0, i); + auto raduis_y = slice(tokens, i + 1); + + css_length rx[4], ry[4]; + int n = parse_1234_lengths(raduis_x, rx, f_length_percentage | f_positive); + int m = parse_1234_lengths(raduis_y, ry, f_length_percentage | f_positive); + + if (n && m) + { + add_four_properties(_border_top_left_radius_x_, rx, n, important); + add_four_properties(_border_top_left_radius_y_, ry, m, important); + } } } -int style::parse_four_lengths(const string& str, css_length len[4]) +bool parse_border_width(const css_token& token, css_length& w) { - string_vector tokens; - split_string(str, tokens, " "); - if (tokens.size() == 0 || tokens.size() > 4) + css_length width; + if (!width.from_token(token, f_length | f_positive, border_width_strings)) + return false; + + if (width.is_predefined()) + width.set_value(border_width_values[width.predef()], css_units_px); + + w = width; + return true; +} + +// https://drafts.csswg.org/css-backgrounds/#propdef-border +// <line-width> || <line-style> || <color> +// <line-width> = <length [0,∞]> | thin | medium | thick +// <line-style> = none | hidden | dotted | dashed | solid | double | groove | ridge | inset | outset +bool parse_border_helper(const css_token_vector& tokens, document_container* container, + css_length& width, border_style& style, web_color& color) +{ + // initial values: https://developer.mozilla.org/en-US/docs/Web/CSS/border#formal_definition + css_length _width = border_width_medium_value; + border_style _style = border_style_none; + web_color _color = web_color::current_color; + + bool width_found = false; + bool style_found = false; + bool color_found = false; + + for (const auto& token : tokens) { - return 0; + if (!width_found && parse_border_width(token, _width)) + width_found = true; + else if (!style_found && parse_keyword(token, _style, border_style_strings)) + style_found = true; + else if (!color_found && parse_color(token, _color, container)) + color_found = true; + else + return false; } + + width = _width; + style = _style; + color = _color; + return true; +} + +void style::parse_border(const css_token_vector& tokens, bool important, document_container* container) +{ + css_length width; + border_style style; + web_color color; + + if (!parse_border_helper(tokens, container, width, style, color)) + return; + + for (auto name : {_border_left_width_, _border_right_width_, _border_top_width_, _border_bottom_width_}) + add_parsed_property(name, property_value(width, important)); + + for (auto name : {_border_left_style_, _border_right_style_, _border_top_style_, _border_bottom_style_}) + add_parsed_property(name, property_value(style, important)); + + for (auto name : {_border_left_color_, _border_right_color_, _border_top_color_, _border_bottom_color_}) + add_parsed_property(name, property_value(color, important)); +} + +// https://drafts.csswg.org/css-backgrounds/#border-shorthands +// border-top, border-right, border-bottom, border-left +void style::parse_border_side(string_id name, const css_token_vector& tokens, bool important, document_container* container) +{ + css_length width; + border_style style; + web_color color; + + if (!parse_border_helper(tokens, container, width, style, color)) + return; + + add_parsed_property(_id(_s(name) + "-width"), property_value(width, important)); + add_parsed_property(_id(_s(name) + "-style"), property_value(style, important)); + add_parsed_property(_id(_s(name) + "-color"), property_value(color, important)); +} + +bool parse_length(const css_token& tok, css_length& length, int options, string keywords) +{ + return length.from_token(tok, options, keywords); +} + +// parses 1 or 2 lengths, but always returns 2 lengths +bool parse_two_lengths(const css_token_vector& tokens, css_length len[2], int options) +{ + auto n = tokens.size(); + if (n != 1 && n != 2) return false; + + css_length a, b; + if (!a.from_token(tokens[0], options)) return false; + if (n == 1) b = a; + + if (n == 2 && !b.from_token(tokens[1], options)) return false; + + len[0] = a; + len[1] = b; + return true; +} + +// parses 1,2,3 or 4 tokens, returns number of tokens parsed or 0 if error +template<class T, class... Args> +int parse_1234_values(const css_token_vector& tokens, T result[4], bool (*parse)(const css_token&, T&, Args...), Args... args) +{ + if (tokens.size() > 4) + return 0; + for (size_t i = 0; i < tokens.size(); i++) { - len[i].fromString(tokens[i]); + if (!parse(tokens[i], result[i], args...)) + return 0; } return (int)tokens.size(); } -void style::parse_background(const string& val, const string& baseurl, bool important, document_container* container) +int parse_1234_lengths(const css_token_vector& tokens, css_length len[4], int options, string keywords) { - string_vector tokens; - split_string(val, tokens, ",", "", "("); - if (tokens.empty()) return; + return parse_1234_values(tokens, len, parse_length, options, keywords); +} + +// This function implements the logic of the kind "if two values are specified, the first one applies to +// top and bottom, the second one to left and right". Works in conjunction with parse_1234_values. +template<class T> +void style::add_four_properties(string_id top_name, T val[4], int n, bool important) +{ + // These always go in trbl order, see comment for "CSS property names" in string_id. + string_id top = top_name; // top-left for corners + string_id right = string_id(top_name + 1); + string_id bottom = string_id(top_name + 2); + string_id left = string_id(top_name + 3); + + // n 4 3 2 1 + // top 0 0 0 0 0 + // right 1 1 1 0 n>1 + // bottom 2 2 0 0 n/3*2 + // left 3 1 1 0 n/2+n/4 + + add_parsed_property(top, property_value(val[0], important)); + add_parsed_property(right, property_value(val[n>1], important)); + add_parsed_property(bottom, property_value(val[n/3*2], important)); + add_parsed_property(left, property_value(val[n/2+n/4], important)); +} + +void style::parse_background(const css_token_vector& tokens, const string& baseurl, bool important, document_container* container) +{ + auto layers = parse_comma_separated_list(tokens); + if (layers.empty()) return; web_color color; - std::vector<background_image> images; - int_vector repeats, origins, clips, attachments; + std::vector<image> images; length_vector x_positions, y_positions; size_vector sizes; - background_gradient grad; + int_vector repeats, attachments, origins, clips; - for (const auto& token : tokens) + for (size_t i = 0; i < layers.size(); i++) { background bg; - if (!parse_one_background(token, container, bg)) + if (!parse_bg_layer(layers[i], container, bg, i == layers.size() - 1)) return; color = bg.m_color; images.push_back(bg.m_image[0]); - repeats.push_back(bg.m_repeat[0]); - origins.push_back(bg.m_origin[0]); - clips.push_back(bg.m_clip[0]); - attachments.push_back(bg.m_attachment[0]); x_positions.push_back(bg.m_position_x[0]); y_positions.push_back(bg.m_position_y[0]); sizes.push_back(bg.m_size[0]); + repeats.push_back(bg.m_repeat[0]); + attachments.push_back(bg.m_attachment[0]); + origins.push_back(bg.m_origin[0]); + clips.push_back(bg.m_clip[0]); } add_parsed_property(_background_color_, property_value(color, important)); add_parsed_property(_background_image_, property_value(images, important)); add_parsed_property(_background_image_baseurl_, property_value(baseurl, important)); - add_parsed_property(_background_repeat_, property_value(repeats, important)); - add_parsed_property(_background_origin_, property_value(origins, important)); - add_parsed_property(_background_clip_, property_value(clips, important)); - add_parsed_property(_background_attachment_, property_value(attachments, important)); add_parsed_property(_background_position_x_, property_value(x_positions, important)); add_parsed_property(_background_position_y_, property_value(y_positions, important)); add_parsed_property(_background_size_, property_value(sizes, important)); + add_parsed_property(_background_repeat_, property_value(repeats, important)); + add_parsed_property(_background_attachment_, property_value(attachments, important)); + add_parsed_property(_background_origin_, property_value(origins, important)); + add_parsed_property(_background_clip_, property_value(clips, important)); } -bool style::parse_one_background(const string& val, document_container* container, background& bg) +// https://drafts.csswg.org/css-backgrounds/#typedef-bg-layer +// <bg-layer> = <bg-image> || <bg-position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <visual-box> || <visual-box> +// <final-bg-layer> = <bg-layer> || <'background-color'> +bool style::parse_bg_layer(const css_token_vector& tokens, document_container* container, background& bg, bool final_layer) { bg.m_color = web_color::transparent; - bg.m_image = { background_image() }; - bg.m_repeat = { background_repeat_repeat }; - bg.m_origin = { background_box_padding }; - bg.m_clip = { background_box_border }; - bg.m_attachment = { background_attachment_scroll }; + bg.m_image = {{}}; bg.m_position_x = { css_length(0, css_units_percentage) }; bg.m_position_y = { css_length(0, css_units_percentage) }; bg.m_size = { css_size(css_length::predef_value(background_size_auto), css_length::predef_value(background_size_auto)) }; - - if(val == "none") - { - return true; - } - - string_vector tokens; - split_string(val, tokens, " \t\n\r", "", "("); + bg.m_repeat = { background_repeat_repeat }; + bg.m_attachment = { background_attachment_scroll }; + bg.m_origin = { background_box_padding }; + bg.m_clip = { background_box_border }; bool color_found = false; bool image_found = false; - bool origin_found = false; - bool clip_found = false; + bool position_found = false; bool repeat_found = false; bool attachment_found = false; + bool origin_found = false; + bool clip_found = false; - string position; - for(const auto& token : tokens) + for (int i = 0; i < (int)tokens.size(); i++) { - int idx; - if(token.substr(0, 3) == "url") - { - if (image_found) return false; - string url; - css::parse_css_url(token, url); - background_image img; - img.type = background_image::bg_image_type_url; - img.url = url; - bg.m_image = { img }; + if (!color_found && final_layer && parse_color(tokens[i], bg.m_color, container)) + color_found = true; + else if (!image_found && parse_bg_image(tokens[i], bg.m_image[0], container)) image_found = true; - } else if( (idx = value_index(token, background_repeat_strings)) >= 0 ) - { - if (repeat_found) return false; - bg.m_repeat = { idx }; + else if (!position_found && parse_bg_position_size(tokens, i, bg.m_position_x[0], bg.m_position_y[0], bg.m_size[0])) + position_found = true, i--; + // Note: double keyword <repeat-style> values are not supported yet. + else if (!repeat_found && parse_keyword(tokens[i], bg.m_repeat[0], background_repeat_strings)) repeat_found = true; - } else if( (idx = value_index(token, background_attachment_strings)) >= 0 ) - { - if (attachment_found) return false; - bg.m_attachment = { idx }; + else if (!attachment_found && parse_keyword(tokens[i], bg.m_attachment[0], background_attachment_strings)) attachment_found = true; - } else if( (idx = value_index(token, background_box_strings)) >= 0 ) - { - if(!origin_found) - { - bg.m_origin = { idx }; - origin_found = true; - } else - { - if (clip_found) return false; - bg.m_clip = { idx }; - clip_found = true; - } - } else if( value_in_list(token, background_position_strings) || - token.find('/') != string::npos || - t_isdigit(token[0]) || - token[0] == '+' || - token[0] == '-' || - token[0] == '.' ) - { - position += " " + token; - } else if (web_color::is_color(token, container)) - { - if (color_found) return false; - bg.m_color = web_color::from_string(token, container); - color_found = true; - } else if ( token.substr(0, 15) == "linear-gradient" || token.substr(0, 25) == "repeating-linear-gradient" || - token.substr(0, 15) == "radial-gradient" || token.substr(0, 25) == "repeating-radial-gradient" || - token.substr(0, 14) == "conic-gradient" || token.substr(0, 24) == "repeating-conic-gradient") - { - if (image_found) return false; - background_image img; - img.type = background_image::bg_image_type_gradient; - css::parse_gradient(token, container, img.gradient); - if(img.is_empty()) return false; - bg.m_image = { img }; - image_found = true; - } + // If one <visual-box> value is present then it sets both background-origin and background-clip to that value. + // If two values are present, then the first sets background-origin and the second background-clip. + else if (!origin_found && parse_keyword(tokens[i], bg.m_origin[0], background_box_strings)) + origin_found = true, bg.m_clip[0] = bg.m_origin[0]; + else if (!clip_found && parse_keyword(tokens[i], bg.m_clip[0], background_box_strings)) + clip_found = true; else - { return false; - } } + return true; +} + +// <bg-position> [ / <bg-size> ]? +bool parse_bg_position_size(const css_token_vector& tokens, int& index, css_length& x, css_length& y, css_size& size) +{ + if (!parse_bg_position(tokens, index, x, y, true)) + return false; + + if (at(tokens, index).ch != '/') + return true; // no [ / <bg-size> ] - if (!position.empty()) + if (!parse_bg_size(tokens, ++index, size)) { - tokens.clear(); - split_string(position, tokens, "/"); + index--; // restore index to point to '/' + return false; // has '/', but <bg-size> failed to parse + } - if (tokens.size() > 2) return false; + return true; // both <bg-position> and <bg-size> parsed successfully +} - if (tokens.size() == 2 && !parse_one_background_size(tokens[1], bg.m_size[0])) - return false; +// https://drafts.csswg.org/css-backgrounds/#typedef-bg-size +// <bg-size> = [ <length-percentage [0,∞]> | auto ]{1,2} | cover | contain +bool parse_bg_size(const css_token_vector& tokens, int& index, css_size& size) +{ + css_length a, b; + + if (!a.from_token(at(tokens, index), f_length_percentage | f_positive, background_size_strings)) + return false; + + // cover | contain + if (a.is_predefined() && a.predef() != background_size_auto) + { + size.width = size.height = a; + index++; + return true; + } - if (!tokens.empty() && !parse_one_background_position(tokens[0], bg.m_position_x[0], bg.m_position_y[0])) + if (b.from_token(at(tokens, index + 1), f_length_percentage | f_positive, "auto")) + index += 2; + else + { + b.predef(background_size_auto); // If only one value is given the second is assumed to be auto. + index++; + } + + size.width = a; + size.height = b; + return true; +} + +bool is_one_of_predef(const css_length& x, int idx1, int idx2) +{ + return x.is_predefined() && is_one_of(x.predef(), idx1, idx2); +} + +// https://drafts.csswg.org/css-backgrounds/#typedef-bg-position +// https://www.w3.org/TR/css-values-4/#position +// <bg-position> = [ left | center | right | top | bottom | <length-percentage> ] | +// [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ] +// Side-relative values (like bottom 10%) are not supported, so only 1 or 2 values are parsed (not 3 or 4). +bool parse_bg_position(const css_token_vector& tokens, int& index, css_length& x, css_length& y, bool convert_keywords_to_percents) +{ + enum { + left = background_position_left, + right = background_position_right, + top = background_position_top, + bottom = background_position_bottom, + center = background_position_center + }; + + css_length a, b; + + if (!a.from_token(at(tokens, index), f_length_percentage, background_position_strings)) + return false; + + if (!b.from_token(at(tokens, index + 1), f_length_percentage, background_position_strings)) + { + // If only one value is specified, the second value is assumed to be center. + b.predef(center); + + // fix wrong order + if (is_one_of_predef(a, top, bottom)) + swap(a, b); + + index++; + } + else // two values + { + // try to fix wrong order + // A pair of keywords can be reordered, while a combination of keyword and length or percentage cannot. + if ((is_one_of_predef(a, top, bottom) && b.is_predefined()) || + (a.is_predefined() && is_one_of_predef(b, left, right))) + swap(a, b); + + // check for wrong order + if (is_one_of_predef(a, top, bottom) || is_one_of_predef(b, left, right)) return false; + + index += 2; + } + + if (convert_keywords_to_percents) + { + if (a.is_predefined()) + a.set_value(background_position_percentages[a.predef()], css_units_percentage); + if (b.is_predefined()) + b.set_value(background_position_percentages[b.predef()], css_units_percentage); } + x = a; + y = b; return true; } -void style::parse_background_image(const string& val, document_container* container, const string& baseurl, bool important) +void style::parse_background_image(const css_token_vector& tokens, const string& baseurl, bool important, document_container* container) { - string_vector tokens; - split_string(val, tokens, ",", "", "("); - if (tokens.empty()) return; + auto layers = parse_comma_separated_list(tokens); + if (layers.empty()) return; + + std::vector<image> images; - std::vector<background_image> images; + for (const auto& layer : layers) + { + image image; + if (layer.size() != 1) + return; + if (!parse_bg_image(layer[0], image, container)) + return; + images.push_back(image); + } - for (auto& token : tokens) + add_parsed_property(_background_image_, property_value(images, important)); + add_parsed_property(_background_image_baseurl_, property_value(baseurl, important)); +} + +// <bg-image> = <image> | none +// <image> = <url> | <gradient> +bool parse_bg_image(const css_token& tok, image& bg_image, document_container* container) +{ + if (tok.ident() == "none") { - trim(token); - if(token.substr(0, 3) == "url") - { - string url; - css::parse_css_url(token, url); - background_image img; - img.type = background_image::bg_image_type_url; - img.url = url; - images.emplace_back(img); - } else if (token.substr(0, 15) == "linear-gradient" || token.substr(0, 25) == "repeating-linear-gradient" || - token.substr(0, 15) == "radial-gradient" || token.substr(0, 25) == "repeating-radial-gradient" || - token.substr(0, 14) == "conic-gradient" || token.substr(0, 24) == "repeating-conic-gradient") - { - background_image img; - img.type = background_image::bg_image_type_gradient; - css::parse_gradient(token, container, img.gradient); - if(!img.is_empty()) - { - images.emplace_back(img); - } - } + bg_image.type = image::type_none; + return true; } - if(!images.empty()) + string url; + if (parse_url(tok, url)) { - add_parsed_property(_background_image_, property_value(images, important)); - add_parsed_property(_background_image_baseurl_, property_value(baseurl, important)); + bg_image.type = image::type_url; + bg_image.url = url; + return true; } + + if (parse_gradient(tok, bg_image.m_gradient, container)) + { + bg_image.type = image::type_gradient; + return true; + } + + return false; } -void style::parse_keyword_comma_list(string_id name, const string& val, bool important) +// https://drafts.csswg.org/css-values-4/#urls +bool parse_url(const css_token& tok, string& url) { - string_vector tokens; - split_string(val, tokens, ","); - if (tokens.empty()) return; + if (tok.type == URL) // legacy syntax without quotes: url(x.com) + { + url = trim(tok.str); + return true; + } + + if (tok.type == CV_FUNCTION && is_one_of(lowcase(tok.name), "url", "src") && + // note: relying on whitespace having been removed from tok.value + tok.value.size() == 1 && tok.value[0].type == STRING) + { + url = trim(tok.value[0].str); + return true; + } + + return false; +} + +void style::parse_keyword_comma_list(string_id name, const css_token_vector& tokens, bool important) +{ + auto layers = parse_comma_separated_list(tokens); + if (layers.empty()) return; int_vector vec; - for (auto& token : tokens) + for (const auto& layer : layers) { - trim(token); - int idx = value_index(token, m_valid_values[name]); - if (idx == -1) return; + int idx; + if (layer.size() != 1) return; + if (!parse_keyword(layer[0], idx, m_valid_values[name])) return; vec.push_back(idx); } add_parsed_property(name, property_value(vec, important)); } -void style::parse_background_position(const string& val, bool important) +void style::parse_background_position(const css_token_vector& tokens, bool important) { - string_vector tokens; - split_string(val, tokens, ","); - if (tokens.empty()) return; + auto layers = parse_comma_separated_list(tokens); + if (layers.empty()) return; length_vector x_positions, y_positions; - for (const auto& token : tokens) + for (const auto& layer : layers) { css_length x, y; - if(!parse_one_background_position(token, x, y)) return; + int index = 0; + if (!parse_bg_position(layer, index, x, y, true) || index != (int)layer.size()) + return; + x_positions.push_back(x); y_positions.push_back(y); } - + add_parsed_property(_background_position_x_, property_value(x_positions, important)); add_parsed_property(_background_position_y_, property_value(y_positions, important)); } -bool style::parse_one_background_position(const string& val, css_length& x, css_length& y) +void style::parse_background_size(const css_token_vector& tokens, bool important) { - string_vector pos; - split_string(val, pos, split_delims_spaces); - - if (pos.empty() || pos.size() > 2) + auto layers = parse_comma_separated_list(tokens); + if (layers.empty()) return; + + size_vector sizes; + + for (const auto& layer : layers) { - return false; + css_size size; + int index = 0; + if (!parse_bg_size(layer, index, size) || index != (int)layer.size()) + return; + + sizes.push_back(size); } - - if (pos.size() == 1) + + add_parsed_property(_background_size_, property_value(sizes, important)); +} + +bool parse_font_weight(const css_token& tok, css_length& weight) +{ + if (int idx = value_index(tok.ident(), font_weight_strings); idx >= 0) { - if (value_in_list(pos[0], "left;right;center")) - { - x.fromString(pos[0], "left;right;center"); - y.set_value(50, css_units_percentage); - } - else if (value_in_list(pos[0], "top;bottom;center")) - { - y.fromString(pos[0], "top;bottom;center"); - x.set_value(50, css_units_percentage); - } - else - { - x.fromString(pos[0], "left;right;center"); - y.set_value(50, css_units_percentage); - } + weight.predef(idx); + return true; } - else if (pos.size() == 2) + + // https://drafts.csswg.org/css-fonts/#font-weight-absolute-values + // Note: fractional values are allowed. + if (tok.type == NUMBER && tok.n.number >= 1 && tok.n.number <= 1000) { - if (value_in_list(pos[0], "left;right")) - { - x.fromString(pos[0], "left;right;center"); - y.fromString(pos[1], "top;bottom;center"); - } - else if (value_in_list(pos[0], "top;bottom")) + weight.set_value(tok.n.number, css_units_none); + return true; + } + + return false; +} + +// <font-style> || <font-variant-css2> || <font-weight> +// None of the allowed values intersect with <font-size>, it cannot consume <font-size>. +// Note: <font-weight> can be a number >=1, but <font-size> inside <font> is <length-percentage>, +// so it can be only 0 number. <font-size> as a standalone property does allow <number>s in quirks mode. +bool parse_font_style_variant_weight(const css_token_vector& tokens, int& index, + int& style, int& variant, css_length& weight) +{ + bool style_found = false; + bool variant_found = false; + bool weight_found = false; + bool res = false; + + int i = index, count = 0; + while (i < (int)tokens.size() && count++ < 3) + { + const auto& tok = tokens[i++]; + // All three properties can have value "normal", and it changes nothing because initial + // values of all three properties are "normal". + if (tok.ident() == "normal") { - x.fromString(pos[1], "left;right;center"); - y.fromString(pos[0], "top;bottom;center"); - } - else if (value_in_list(pos[1], "left;right")) + index++; + res = true; + } else if (!style_found && parse_keyword(tok, style, font_style_strings)) { - x.fromString(pos[1], "left;right;center"); - y.fromString(pos[0], "top;bottom;center"); + style_found = true; + index++; + res = true; } - else if (value_in_list(pos[1], "top;bottom")) + else if (!variant_found && parse_keyword(tok, variant, font_variant_strings)) { - x.fromString(pos[0], "left;right;center"); - y.fromString(pos[1], "top;bottom;center"); + variant_found = true; + index++; + res = true; } - else + else if (!weight_found && parse_font_weight(tok, weight)) { - x.fromString(pos[0], "left;right;center"); - y.fromString(pos[1], "top;bottom;center"); - } + weight_found = true; + index++; + res = true; + } else break; } + return res; +} - if (x.is_predefined()) +// https://www.w3.org/TR/css-values-4/#custom-idents +bool is_custom_ident(const css_token& tok) +{ + if (tok.type != IDENT) return false; + // Custom identifiers are case-sensitive, but they should not case-insensitively match any of + // CSS-wide keywords or "default". + return !is_one_of(lowcase(tok.name), "default", "initial", "inherit", "unset"); +} + +// https://drafts.csswg.org/css-fonts/#propdef-font-family +// font-family = [ <family-name> | <generic-family> ]# +// <family-name> = <string> | <custom-ident>+ +// <generic-family> = generic( <custom-ident>+ ) | <string> | <custom-ident>+ +bool parse_font_family(const css_token_vector& tokens, string& font_family) +{ + auto list = parse_comma_separated_list(tokens); + if (list.empty()) return false; + + string result; + for (const auto& name : list) { - switch (x.predef()) + if (name.size() == 1 && name[0].type == STRING) { - case 0: - x.set_value(0, css_units_percentage); - break; - case 1: - x.set_value(100, css_units_percentage); - break; - case 2: - x.set_value(50, css_units_percentage); - break; + //result.push_back(name[0].str); + result += name[0].str + ','; + continue; } - } - if (y.is_predefined()) - { - switch (y.predef()) + + // Otherwise: name must be a list of <custom-ident>s + // Note: generic( <custom-ident>+ ) is not supported + string str; + for (const auto& tok : name) { - case 0: - y.set_value(0, css_units_percentage); - break; - case 1: - y.set_value(100, css_units_percentage); - break; - case 2: - y.set_value(50, css_units_percentage); - break; + if (!is_custom_ident(tok)) return false; + str += tok.name + ' '; } + //result.push_back(trim(str)); + result += trim(str) + ','; } + result.resize(result.size() - 1); // remove last ',' + font_family = result; return true; } -void style::parse_background_size(const string& val, bool important) +// https://developer.mozilla.org/en-US/docs/Web/CSS/font +// https://drafts.csswg.org/css-fonts/#font-prop +// font = <font-style-weight>? <font-size> [ / <line-height> ]? <font-family> +// font = <system-family-name*> +// <font-style-weight> = <font-style> || <font-variant-css2> || <font-weight> || <font-width-css3*> +// values marked * are not supported +void style::parse_font(css_token_vector tokens, bool important) { - string_vector tokens; - split_string(val, tokens, ","); - if (tokens.empty()) return; - - size_vector sizes; - - for (const auto& token : tokens) + // initial values + int style = font_style_normal; + int variant = font_variant_normal; + css_length weight = css_length::predef_value(font_weight_normal); + css_length size = css_length::predef_value(font_size_medium); + css_length line_height = css_length::predef_value(line_height_normal); + string font_family; // this argument is mandatory, no need to set initial value + + if(tokens.size() == 1 && (tokens[0].type == STRING || tokens[0].type == IDENT) && value_in_list(tokens[0].str, font_system_family_name_strings)) { - css_size size; - if (!parse_one_background_size(token, size)) return; - sizes.push_back(size); - } + font_family = tokens[0].str; + } else + { + int index = 0; + parse_font_style_variant_weight(tokens, index, style, variant, weight); - add_parsed_property(_background_size_, property_value(sizes, important)); -} + // font-size = <absolute-size> | <relative-size> | <length-percentage [0,∞]> | math + if (!size.from_token(at(tokens, index), f_length_percentage | f_positive, font_size_strings)) + return; + index++; -bool style::parse_one_background_size(const string& val, css_size& size) -{ - string_vector res; - split_string(val, res, split_delims_spaces); - if (res.empty()) - { - return false; - } + if (at(tokens, index).ch == '/') + { + index++; + // https://drafts.csswg.org/css2/#propdef-line-height + // line-height = normal | <number> | <length> | <percentage> + if (!line_height.from_token(at(tokens, index), f_number | f_length_percentage, line_height_strings)) + return; + index++; + } - size.width.fromString(res[0], background_size_strings); - if (res.size() > 1) - { - size.height.fromString(res[1], background_size_strings); - } - else - { - size.height.predef(background_size_auto); + remove(tokens, 0, index); + if (!parse_font_family(tokens, font_family)) + return; } - return true; + add_parsed_property(_font_style_, property_value(style, important)); + add_parsed_property(_font_variant_, property_value(variant, important)); + add_parsed_property(_font_weight_, property_value(weight, important)); + add_parsed_property(_font_size_, property_value(size, important)); + add_parsed_property(_line_height_, property_value(line_height, important)); + add_parsed_property(_font_family_, property_value(font_family, important)); } -void style::parse_font(const string& val, bool important) +// https://developer.mozilla.org/en-US/docs/Web/CSS/flex +// https://drafts.csswg.org/css-flexbox/#flex-property +// flex = none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] +void style::parse_flex(const css_token_vector& tokens, bool important) { - add_parsed_property(_font_style_, property_value(font_style_normal, important)); - add_parsed_property(_font_variant_, property_value(font_variant_normal, important)); - add_parsed_property(_font_weight_, property_value(font_weight_normal, important)); - add_parsed_property(_font_size_, property_value(font_size_medium, important)); - add_parsed_property(_line_height_, property_value(line_height_normal, important)); - - string_vector tokens; - split_string(val, tokens, " ", "", "\""); - - int idx; - bool is_family = false; - string font_family; - for(const auto& token : tokens) + auto n = tokens.size(); + if (n > 3) return; + + const auto& a = at(tokens, 0); + const auto& b = at(tokens, 1); + const auto& c = at(tokens, 2); + + struct flex { - if(is_family) - { - font_family += token; - continue; - } + float m_grow = 1; // flex-grow is set to 1 when omitted + float m_shrink = 1; + css_length m_basis = 0; - if((idx = value_index(token, font_style_strings)) >= 0) + bool grow(const css_token& tok) { - if(idx == 0) - { - add_parsed_property(_font_style_, property_value(font_style_normal, important)); - add_parsed_property(_font_variant_, property_value(font_variant_normal, important)); - add_parsed_property(_font_weight_, property_value(font_weight_normal, important)); - } else - { - add_parsed_property(_font_style_, property_value(idx, important)); - } - } else if((idx = value_index(token, font_weight_strings)) >= 0) + if (tok.type != NUMBER || tok.n.number < 0) return false; + m_grow = tok.n.number; + return true; + } + bool shrink(const css_token& tok) { - add_parsed_property(_font_weight_, property_value(idx, important)); - } else if((idx = value_index(token, font_variant_strings)) >= 0) + if (tok.type != NUMBER || tok.n.number < 0) return false; + m_shrink = tok.n.number; + return true; + } + // unitless_zero_allowed enforces the rule "A unitless zero that is not already preceded by two flex factors must be interpreted as a flex factor." + bool basis(const css_token& tok, bool unitless_zero_allowed = false) { - add_parsed_property(_font_variant_, property_value(idx, important)); + if (!unitless_zero_allowed && tok.type == NUMBER && tok.n.number == 0) + return false; + return m_basis.from_token(tok, f_length_percentage | f_positive, flex_basis_strings); } - else if(t_isdigit(token[0]) || token[0] == '.' || - value_in_list(token, font_size_strings) || token.find('/') != string::npos) + }; + flex flex; + + if (n == 1) + { + string_id ident = _id(a.ident()); + if (is_one_of(ident, _initial_, _auto_, _none_)) { - string_vector szlh; - split_string(token, szlh, "/"); - if(!szlh.empty()) + css_length _auto = css_length::predef_value(flex_basis_auto); + + switch (ident) { - auto size = css_length::from_string(szlh[0], font_size_strings, -1); - add_parsed_property(_font_size_, property_value(size, important)); - - if (szlh.size() == 2) - { - auto height = css_length::from_string(szlh[1], "normal", -1); - add_parsed_property(_line_height_, property_value(height, important)); - } + case _initial_: flex = {0, 1, _auto}; break; + case _auto_: flex = {1, 1, _auto}; break; // can be handled by else + case _none_: flex = {0, 0, _auto}; break; + default:; } - } else + } + else { - is_family = true; - font_family += token; + bool ok = flex.grow(a) || flex.basis(a); + if (!ok) return; } } - add_parsed_property(_font_family_, property_value(font_family, important)); -} - -void style::parse_flex(const string& val, bool important) -{ - css_length _auto = css_length::predef_value(flex_basis_auto); - - if (val == "initial") - { - // 0 1 auto - add_parsed_property(_flex_grow_, property_value(0.f, important)); - add_parsed_property(_flex_shrink_, property_value(1.f, important)); - add_parsed_property(_flex_basis_, property_value(_auto, important)); - } - else if (val == "auto") + else if (n == 2) { - // 1 1 auto - add_parsed_property(_flex_grow_, property_value(1.f, important)); - add_parsed_property(_flex_shrink_, property_value(1.f, important)); - add_parsed_property(_flex_basis_, property_value(_auto, important)); + // <number> <number> + // <number> <basis> + // <basis> <number> + bool ok = + (flex.grow(a) && (flex.shrink(b) || flex.basis(b))) || + (flex.basis(a) && flex.grow(b)); + + if (!ok) return; } - else if (val == "none") + else // n == 3 { - // 0 0 auto - add_parsed_property(_flex_grow_, property_value(0.f, important)); - add_parsed_property(_flex_shrink_, property_value(0.f, important)); - add_parsed_property(_flex_basis_, property_value(_auto, important)); + // <number> <number> <basis> + // <basis> <number> <number> + bool ok = + (flex.grow(a) && flex.shrink(b) && flex.basis(c, true)) || + (flex.basis(a) && flex.grow(b) && flex.shrink(c)); + + if (!ok) return; } - else + + add_parsed_property(_flex_grow_, property_value(flex.m_grow, important)); + add_parsed_property(_flex_shrink_, property_value(flex.m_shrink, important)); + add_parsed_property(_flex_basis_, property_value(flex.m_basis, important)); +} + +// flex-flow = <'flex-direction'> || <'flex-wrap'> +void style::parse_flex_flow(const css_token_vector& tokens, bool important) +{ + // initial values: https://developer.mozilla.org/en-US/docs/Web/CSS/flex-flow#formal_definition + int flex_direction = flex_direction_row; + int flex_wrap = flex_wrap_nowrap; + + bool direction_found = false; + bool wrap_found = false; + + for (const auto& token : tokens) { - string_vector tokens; - split_string(val, tokens, " "); - if (tokens.size() == 3) - { - float grow = t_strtof(tokens[0]); - float shrink = t_strtof(tokens[1]); - auto basis = css_length::from_string(tokens[2], flex_basis_strings, -1); - if(!basis.is_predefined() && basis.units() == css_units_none && basis.val() == 0) - { - basis.set_value(basis.val(), css_units_px); - } - - add_parsed_property(_flex_grow_, property_value(grow, important)); - add_parsed_property(_flex_shrink_, property_value(shrink, important)); - add_parsed_property(_flex_basis_, property_value(basis, important)); - } - else if (tokens.size() == 2) - { - float grow = t_strtof(tokens[0]); - add_parsed_property(_flex_grow_, property_value(grow, important)); - - if (litehtml::is_number(tokens[1])) - { - float shrink = t_strtof(tokens[1]); - add_parsed_property(_flex_shrink_, property_value(shrink, important)); - add_parsed_property(_flex_basis_, property_value(css_length(0), important)); - } - else - { - auto basis = css_length::from_string(tokens[1], flex_basis_strings, -1); - add_parsed_property(_flex_basis_, property_value(basis, important)); - } - } - else if (tokens.size() == 1) - { - if (is_number(tokens[0])) - { - float grow = t_strtof(tokens[0]); - add_parsed_property(_flex_grow_, property_value(grow, important)); - add_parsed_property(_flex_shrink_, property_value(1.f, important)); - add_parsed_property(_flex_basis_, property_value(css_length(0), important)); - } - else - { - auto basis = css_length::from_string(tokens[0], flex_basis_strings, -1); - add_parsed_property(_flex_grow_, property_value(1.f, important)); - add_parsed_property(_flex_shrink_, property_value(1.f, important)); - add_parsed_property(_flex_basis_, property_value(basis, important)); - } - } + if (!direction_found && parse_keyword(token, flex_direction, flex_direction_strings)) + direction_found = true; + else if (!wrap_found && parse_keyword(token, flex_wrap, flex_wrap_strings)) + wrap_found = true; + else + return; } + + add_parsed_property(_flex_direction_, property_value(flex_direction, important)); + add_parsed_property(_flex_wrap_, property_value(flex_wrap, important)); } -void style::parse_align_self(string_id name, const string& val, bool important) +// https://www.w3.org/TR/css-align/#align-self-property +// value = auto | normal | stretch | [ [first | last]? && baseline ] | [safe | unsafe]? <self-position> +// <self-position> = center | start | end | self-start | self-end | flex-start | flex-end +// https://www.w3.org/TR/css-align/#align-items-property +// same as align-self, except auto is not allowed +void style::parse_align_self(string_id name, const css_token_vector& tokens, bool important) { - string_vector tokens; - split_string(val, tokens, " "); - if(tokens.size() == 1) + auto n = tokens.size(); + if (n > 2) + return; + if (tokens[0].type != IDENT || (n == 2 && tokens[1].type != IDENT)) + return; + + string a = tokens[0].ident(); + + if (name == _align_items_ && a == "auto") + return; + + if (n == 1) { - int idx = value_index(val, m_valid_values[name]); + int idx = value_index(a, flex_align_items_strings); if (idx >= 0) - { add_parsed_property(name, property_value(idx, important)); - } - } else + return; + } + + // Otherwise: n == 2 + + string b = tokens[1].ident(); + + if (a == "baseline") swap(a, b); + if (b == "baseline" && is_one_of(a, "first", "last")) { - int val1 = 0; - int val2 = -1; - for(auto &token : tokens) - { - if(token == "first") - { - val1 |= flex_align_items_first; - } else if(token == "last") - { - val1 |= flex_align_items_last; - } else if(token == "safe") - { - val1 |= flex_align_items_safe; - } else if(token == "unsafe") - { - val1 |= flex_align_items_unsafe; - } else - { - int idx = value_index(token, m_valid_values[name]); - if(idx >= 0) - { - val2 = idx; - } - } - } - if(val2 >= 0) - { - add_parsed_property(name, property_value(val1 | val2, important)); - } + int idx = flex_align_items_baseline | (a == "first" ? flex_align_items_first : flex_align_items_last); + add_parsed_property(name, property_value(idx, important)); + return; + } + + // <overflow-position> <self-position> + int idx = value_index(b, self_position_strings); + if (idx >= 0 && is_one_of(a, "safe", "unsafe")) + { + idx |= (a == "safe" ? flex_align_items_safe : flex_align_items_unsafe); + add_parsed_property(name, property_value(idx, important)); } } @@ -1234,24 +1433,70 @@ const property_value& style::get_property(string_id name) const { return it->second; } - static property_value _invalid(invalid(), false); - return _invalid; + static property_value dummy; + return dummy; +} + +// var( <custom-property-name> , <declaration-value>? ) +bool check_var_syntax(const css_token_vector& args) +{ + if (args.empty()) return false; + + string name = args[0].ident(); + if (name.substr(0, 2) != "--" || name.size() <= 2) + return false; + + if (args.size() > 1 && args[1].ch != ',') + return false; + if (args.size() > 2 && !is_declaration_value(args, 2)) + return false; + + return true; } -void style::subst_vars_(string& str, const html_tag* el) +// https://drafts.csswg.org/css-variables/#using-variables +// var( <custom-property-name> , <declaration-value>? ) +// returns true if one var() was substituted +// returns true if there was error or var() was not found +bool subst_var(css_token_vector& tokens, const html_tag* el, std::set<string_id>& used_vars) { - while (true) + for (int i = 0; i < (int)tokens.size(); i++) { - auto start = str.find("var("); - if (start == string::npos) break; - if (start > 0 && isalnum(str[start - 1])) break; - auto end = str.find(')', start + 4); - if (end == string::npos) break; - auto name = str.substr(start + 4, end - start - 4); - trim(name); - string val = el->get_custom_property(_id(name), ""); - str.replace(start, end - start + 1, val); + auto& tok = tokens[i]; + if (tok.type == CV_FUNCTION && lowcase(tok.name) == "var") + { + auto args = tok.value; // copy is intentional + if (!check_var_syntax(args)) return false; + + auto name = _id(args[0].name); + if (name in used_vars) return false; // dependency cycle https://drafts.csswg.org/css-variables/#cycles + used_vars.insert(name); + + css_token_vector value; + if (el->get_custom_property(name, value)) + { + remove(tokens, i); + insert(tokens, i, value); + } + else // custom property not defined + { + if (args.size() == 1) return false; // default value not provided + remove(args, 0, 2); + remove(tokens, i); + insert(tokens, i, args); + } + return true; + } + if (tok.is_component_value() && subst_var(tok.value, el, used_vars)) + return true; } + return false; +} + +void subst_vars_(string_id name, css_token_vector& tokens, const html_tag* el) +{ + std::set<string_id> used_vars = {name}; + while (subst_var(tokens, el, used_vars)); } void style::subst_vars(const html_tag* el) @@ -1260,12 +1505,12 @@ void style::subst_vars(const html_tag* el) { if (prop.second.m_has_var) { - string str = prop.second.get<string>(); - subst_vars_(str, el); + auto& value = prop.second.get<css_token_vector>(); + subst_vars_(prop.first, value, el); // re-adding the same property - // if it is a custom property it will be readded as a string + // if it is a custom property it will be readded as a css_token_vector // if it is a standard css property it will be parsed and properly added as typed property - add_property(prop.first, str, "", prop.second.m_important, el->get_document()->container()); + add_property(prop.first, value, "", prop.second.m_important, el->get_document()->container()); } } } |
