#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
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 style::m_valid_values =
{
{ _display_, style_display_strings },
{ _visibility_, visibility_strings },
{ _position_, element_position_strings },
{ _float_, element_float_strings },
{ _clear_, element_clear_strings },
{ _overflow_, overflow_strings },
{ _box_sizing_, box_sizing_strings },
{ _text_align_, text_align_strings },
{ _vertical_align_, vertical_align_strings },
{ _text_transform_, text_transform_strings },
{ _white_space_, white_space_strings },
{ _font_style_, font_style_strings },
{ _font_variant_, font_variant_strings },
{ _font_weight_, font_weight_strings },
{ _list_style_type_, list_style_type_strings },
{ _list_style_position_, list_style_position_strings },
{ _border_left_style_, border_style_strings },
{ _border_right_style_, border_style_strings },
{ _border_top_style_, border_style_strings },
{ _border_bottom_style_, border_style_strings },
{ _border_collapse_, border_collapse_strings },
// these 4 properties are comma-separated lists of keywords, see parse_keyword_comma_list
{ _background_attachment_, background_attachment_strings },
{ _background_repeat_, background_repeat_strings },
{ _background_clip_, background_box_strings },
{ _background_origin_, background_box_strings },
{ _flex_direction_, flex_direction_strings },
{ _flex_wrap_, flex_wrap_strings },
{ _justify_content_, flex_justify_content_strings },
{ _align_content_, flex_align_content_strings },
{ _align_items_, flex_align_items_strings },
{ _align_self_, flex_align_items_strings },
{ _caption_side_, caption_side_strings },
};
std::map> shorthands =
{
{ _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_}},
};
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)
{
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);
}
}
bool has_var(const css_token_vector& tokens)
{
for (auto& tok : tokens)
{
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)
{
auto atomic_properties = at(shorthands, name);
if (!atomic_properties.empty())
{
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_length_property(string_id name, css_token val, string keywords, int options, bool important)
{
css_length length;
if (length.from_token(val, options, keywords))
add_parsed_property(name, property_value(length, important));
}
// `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)
{
// ============================= SINGLE KEYWORD =============================
case _display_:
case _visibility_:
case _position_:
case _float_:
case _clear_:
case _box_sizing_:
case _overflow_:
case _text_align_:
case _vertical_align_:
case _text_transform_:
case _white_space_:
case _font_style_:
case _font_variant_:
case _list_style_type_:
case _list_style_position_:
case _border_top_style_:
case _border_bottom_style_:
case _border_left_style_:
case _border_right_style_:
case _border_collapse_:
case _flex_direction_:
case _flex_wrap_:
case _justify_content_:
case _align_content_:
case _caption_side_:
if (int index = value_index(ident, m_valid_values[name]); index >= 0)
add_parsed_property(name, property_value(index, important));
break;
// ============================= LENGTH =============================
// auto | 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);
// 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);
// https://developer.mozilla.org/en-US/docs/Web/CSS/padding-left
case _padding_left_:
case _padding_right_:
case _padding_top_:
case _padding_bottom_:
return add_length_property(name, val, "", f_length_percentage|f_positive, important);
// auto | https://developer.mozilla.org/en-US/docs/Web/CSS/left
case _left_:
case _right_:
case _top_:
case _bottom_:
case _margin_left_:
case _margin_right_:
case _margin_top_:
case _margin_bottom_:
return add_length_property(name, val, "auto", f_length_percentage, important);
// auto | min-content | max-content | fit-content | 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);
// none | min-content | max-content | fit-content | https://developer.mozilla.org/en-US/docs/Web/CSS/max-width#formal_syntax
case _max_width_:
case _max_height_:
return add_length_property(name, val, "none", f_length_percentage|f_positive, important);
// normal | | https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#formal_syntax
case _line_height_:
return add_length_property(name, val, "normal", f_number|f_length_percentage|f_positive, important);
// font-size = | | https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#formal_syntax
case _font_size_:
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;
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 _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;
// ============================= 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(value, important);
break;
case _background_size_:
parse_background_size(value, important);
break;
case _background_repeat_:
case _background_attachment_:
case _background_origin_:
case _background_clip_:
parse_keyword_comma_list(name, value, important);
break;
// ============================= BORDER =============================
case _border_:
parse_border(value, important, container);
break;
case _border_left_:
case _border_right_:
case _border_top_:
case _border_bottom_:
parse_border_side(name, value, important, container);
break;
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_:
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_:
if (parse_border_width(val, *len))
add_parsed_property(name, property_value(*len, important));
break;
// border-bottom-left-radius = {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_:
if (parse_two_lengths(value, len, f_length_percentage | f_positive))
{
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 = name == _border_radius_x_ ?
_border_top_left_radius_x_ :
_border_top_left_radius_y_;
if (int n = parse_1234_lengths(value, len, f_length_percentage | f_positive))
add_four_properties(top_left, len, n, important);
break;
}
case _border_radius_:
parse_border_radius(value, important);
break;
// border-spacing = {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))
{
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;
// ============================= LIST =============================
case _list_style_image_:
if (string url; parse_list_style_image(val, url))
{
add_parsed_property(_list_style_image_, property_value(url, important));
add_parsed_property(_list_style_image_baseurl_, property_value(baseurl, important));
}
break;
case _list_style_:
parse_list_style(value, baseurl, important);
break;
// ============================= FONT =============================
case _font_:
parse_font(value, important);
break;
case _font_family_:
if (parse_font_family(value, str))
add_parsed_property(name, property_value(str, important));
break;
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(value, important);
break;
case _flex_grow_: //
case _flex_shrink_:
if (val.type == NUMBER && val.n.number >= 0)
add_parsed_property(name, property_value(val.n.number, important));
break;
case _flex_basis_:
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_: //
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_:
{
// 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:
if (_s(name).substr(0, 2) == "--" && _s(name).size() >= 3 &&
(value.empty() || is_declaration_value(value)))
add_parsed_property(name, property_value(value, important));
}
}
void style::add_property(string_id name, const string& value, const string& baseurl, bool important, document_container* container)
{
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")
{
url = "";
return true;
}
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)
{
// "...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
}
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));
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius
// border-radius = {1,4} [ / {1,4} ]?
void style::parse_border_radius(const css_token_vector& tokens, bool important)
{
int i;
for (i = 0; i < (int)tokens.size() && tokens[i].ch != '/'; i++) {}
if (i == (int)tokens.size()) // no '/'
{
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
{
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);
}
}
}
bool parse_border_width(const css_token& token, css_length& w)
{
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
// || ||
// = | thin | medium | thick
// = 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)
{
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
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++)
{
if (!parse(tokens[i], result[i], args...))
return 0;
}
return (int)tokens.size();
}
int parse_1234_lengths(const css_token_vector& tokens, css_length len[4], int options, string keywords)
{
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
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 images;
length_vector x_positions, y_positions;
size_vector sizes;
int_vector repeats, attachments, origins, clips;
for (size_t i = 0; i < layers.size(); i++)
{
background 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]);
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_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));
}
// https://drafts.csswg.org/css-backgrounds/#typedef-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 = {{}};
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)) };
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 position_found = false;
bool repeat_found = false;
bool attachment_found = false;
bool origin_found = false;
bool clip_found = false;
for (int i = 0; i < (int)tokens.size(); i++)
{
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 (!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 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 (!attachment_found && parse_keyword(tokens[i], bg.m_attachment[0], background_attachment_strings))
attachment_found = true;
// If one 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;
}
// [ / ]?
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 [ / ]
if (!parse_bg_size(tokens, ++index, size))
{
index--; // restore index to point to '/'
return false; // has '/', but failed to parse
}
return true; // both and parsed successfully
}
// https://drafts.csswg.org/css-backgrounds/#typedef-bg-size
// = [ | 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 (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
// = [ left | center | right | top | bottom | ] |
// [ left | center | right | ] [ top | center | bottom | ]
// 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 css_token_vector& tokens, const string& baseurl, bool important, document_container* container)
{
auto layers = parse_comma_separated_list(tokens);
if (layers.empty()) return;
std::vector 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);
}
add_parsed_property(_background_image_, property_value(images, important));
add_parsed_property(_background_image_baseurl_, property_value(baseurl, important));
}
// = | none
// = |
bool parse_bg_image(const css_token& tok, image& bg_image, document_container* container)
{
if (tok.ident() == "none")
{
bg_image.type = image::type_none;
return true;
}
string url;
if (parse_url(tok, url))
{
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;
}
// https://drafts.csswg.org/css-values-4/#urls
bool parse_url(const css_token& tok, string& url)
{
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 (const auto& layer : layers)
{
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 css_token_vector& tokens, bool important)
{
auto layers = parse_comma_separated_list(tokens);
if (layers.empty()) return;
length_vector x_positions, y_positions;
for (const auto& layer : layers)
{
css_length x, y;
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));
}
void style::parse_background_size(const css_token_vector& tokens, bool important)
{
auto layers = parse_comma_separated_list(tokens);
if (layers.empty()) return;
size_vector sizes;
for (const auto& layer : layers)
{
css_size size;
int index = 0;
if (!parse_bg_size(layer, index, size) || index != (int)layer.size())
return;
sizes.push_back(size);
}
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)
{
weight.predef(idx);
return true;
}
// 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)
{
weight.set_value(tok.n.number, css_units_none);
return true;
}
return false;
}
// || ||
// None of the allowed values intersect with , it cannot consume .
// Note: can be a number >=1, but inside is ,
// so it can be only 0 number. as a standalone property does allow 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")
{
index++;
res = true;
} else if (!style_found && parse_keyword(tok, style, font_style_strings))
{
style_found = true;
index++;
res = true;
}
else if (!variant_found && parse_keyword(tok, variant, font_variant_strings))
{
variant_found = true;
index++;
res = true;
}
else if (!weight_found && parse_font_weight(tok, weight))
{
weight_found = true;
index++;
res = true;
} else break;
}
return res;
}
// 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 = [ | ]#
// = | +
// = generic( + ) | | +
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)
{
if (name.size() == 1 && name[0].type == STRING)
{
//result.push_back(name[0].str);
result += name[0].str + ',';
continue;
}
// Otherwise: name must be a list of s
// Note: generic( + ) is not supported
string str;
for (const auto& tok : name)
{
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;
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
// https://drafts.csswg.org/css-fonts/#font-prop
// font = ? [ / ]?
// font =
// = || || ||
// values marked * are not supported
void style::parse_font(css_token_vector tokens, bool important)
{
// 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))
{
font_family = tokens[0].str;
} else
{
int index = 0;
parse_font_style_variant_weight(tokens, index, style, variant, weight);
// font-size = | | | math
if (!size.from_token(at(tokens, index), f_length_percentage | f_positive, font_size_strings))
return;
index++;
if (at(tokens, index).ch == '/')
{
index++;
// https://drafts.csswg.org/css2/#propdef-line-height
// line-height = normal | | |
if (!line_height.from_token(at(tokens, index), f_number | f_length_percentage, line_height_strings))
return;
index++;
}
remove(tokens, 0, index);
if (!parse_font_family(tokens, font_family))
return;
}
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));
}
// 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)
{
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
{
float m_grow = 1; // flex-grow is set to 1 when omitted
float m_shrink = 1;
css_length m_basis = 0;
bool grow(const css_token& tok)
{
if (tok.type != NUMBER || tok.n.number < 0) return false;
m_grow = tok.n.number;
return true;
}
bool shrink(const css_token& tok)
{
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)
{
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);
}
};
flex flex;
if (n == 1)
{
string_id ident = _id(a.ident());
if (is_one_of(ident, _initial_, _auto_, _none_))
{
css_length _auto = css_length::predef_value(flex_basis_auto);
switch (ident)
{
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
{
bool ok = flex.grow(a) || flex.basis(a);
if (!ok) return;
}
}
else if (n == 2)
{
//
//
//
bool ok =
(flex.grow(a) && (flex.shrink(b) || flex.basis(b))) ||
(flex.basis(a) && flex.grow(b));
if (!ok) return;
}
else // n == 3
{
//
//
bool ok =
(flex.grow(a) && flex.shrink(b) && flex.basis(c, true)) ||
(flex.basis(a) && flex.grow(b) && flex.shrink(c));
if (!ok) return;
}
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)
{
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));
}
// https://www.w3.org/TR/css-align/#align-self-property
// value = auto | normal | stretch | [ [first | last]? && baseline ] | [safe | unsafe]?
// = 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)
{
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(a, flex_align_items_strings);
if (idx >= 0)
add_parsed_property(name, property_value(idx, important));
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 idx = flex_align_items_baseline | (a == "first" ? flex_align_items_first : flex_align_items_last);
add_parsed_property(name, property_value(idx, important));
return;
}
//
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));
}
}
void style::add_parsed_property( string_id name, const property_value& propval )
{
auto prop = m_properties.find(name);
if (prop != m_properties.end())
{
if (!prop->second.m_important || (propval.m_important && prop->second.m_important))
{
prop->second = propval;
}
}
else
{
m_properties[name] = propval;
}
}
void style::remove_property( string_id name, bool important )
{
auto prop = m_properties.find(name);
if(prop != m_properties.end())
{
if( !prop->second.m_important || (important && prop->second.m_important) )
{
m_properties.erase(prop);
}
}
}
void style::combine(const style& src)
{
for (const auto& property : src.m_properties)
{
add_parsed_property(property.first, property.second);
}
}
const property_value& style::get_property(string_id name) const
{
auto it = m_properties.find(name);
if (it != m_properties.end())
{
return it->second;
}
static property_value dummy;
return dummy;
}
// var( , ? )
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;
}
// https://drafts.csswg.org/css-variables/#using-variables
// var( , ? )
// 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& used_vars)
{
for (int i = 0; i < (int)tokens.size(); i++)
{
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 used_vars = {name};
while (subst_var(tokens, el, used_vars));
}
void style::subst_vars(const html_tag* el)
{
for (auto& prop : m_properties)
{
if (prop.second.m_has_var)
{
auto& value = prop.second.get();
subst_vars_(prop.first, value, el);
// re-adding the same property
// 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, value, "", prop.second.m_important, el->get_document()->container());
}
}
}
} // namespace litehtml