#include "html.h" #include "gradient.h" #include "css_parser.h" namespace litehtml { bool parse_linear_gradient_direction(const css_token_vector& tokens, int& index, float& angle, int& side); bool parse_linear_gradient_direction_and_interpolation(const css_token_vector& tokens, gradient& gradient); bool parse_color_interpolation_method(const css_token_vector& tokens, int& index, color_space_t& color_space, hue_interpolation_t& hue_interpolation); bool parse_gradient_position(const css_token_vector& tokens, int& index, gradient& gradient); bool parse_radial_gradient_shape_size_position_interpolation(const css_token_vector& tokens, gradient& result); bool parse_conic_gradient_angle_position_interpolation(const css_token_vector& tokens, gradient& gradient); template bool parse_color_stop_list(const vector& list, gradient& grad, document_container* container); //////////////////////////////////////////////////////////////////////////////////////////// // These combinators are currently used only in one place because the code is usually shorter without them. using parse_fn = std::function; // a? parse_fn opt(parse_fn a) { return [=](auto&... x) { a(x...); return true; }; } // a b parse_fn seq(parse_fn a, parse_fn b) { return [=](auto& t, auto& i) { auto save = i; bool result = a(t, i) && b(t, i); if (!result) i = save; // backtrack return result; }; } // Not overloading operator|| because it is easier to get a bug: a || b || c does the wrong thing, // see the note at https://www.w3.org/TR/css-values-4/#component-combinators. // a || b parse_fn oror(parse_fn a, parse_fn b) { return [=](auto&... x) { if (a(x...)) { b(x...); return true; } else if (b(x...)) { a(x...); return true; } return false; }; } parse_fn operator""_x(const char* str, size_t len) { return [=](const css_token_vector& tokens, int& index) { if (at(tokens, index).ident() == string(str, len)) { index++; return true; } return false; }; } bool end(const css_token_vector& tokens, int index) { return index == (int)tokens.size(); } //////////////////////////////////////////////////////////////////////////////////////////// // https://drafts.csswg.org/css-images-4/#gradients // // = // | | // | | // | // bool parse_gradient(const css_token& token, gradient& result, document_container* container) { if (token.type != CV_FUNCTION) return false; auto type = _id(lowcase(token.name)); if (!is_one_of(type, _linear_gradient_, _repeating_linear_gradient_, _radial_gradient_, _repeating_radial_gradient_, _conic_gradient_, _repeating_conic_gradient_)) return false; gradient grad(type); if (!grad.is_linear()) { // radial and conic position defaults to 'center' // https://drafts.csswg.org/css-images-3/#valdef-radial-gradient-position // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-position grad.m_side = gradient_side_x_center | gradient_side_y_center; } auto list = parse_comma_separated_list(token.value); if (list.empty()) return false; bool ok; if (grad.is_linear()) ok = parse_linear_gradient_direction_and_interpolation(list[0], grad); else if (grad.is_radial()) ok = parse_radial_gradient_shape_size_position_interpolation(list[0], grad); else ok = parse_conic_gradient_angle_position_interpolation(list[0], grad); if (ok) remove(list, 0); if (grad.is_conic()) ok = parse_color_stop_list(list, grad, container); else ok = parse_color_stop_list(list, grad, container); if (!ok) return false; result = grad; return true; } // parse or bool parse_lenang(const css_token& tok, css_length& length) { return parse_length(tok, length, f_length_percentage); } bool parse_lenang(const css_token& tok, float& angle) { return parse_angle(tok, angle, true); } // = | template // T == css_length or float bool parse_color_hint(const css_token_vector& tokens, vector& color_stops) { T lenang; if (tokens.size() == 1 && parse_lenang(tokens[0], lenang)) { color_stops.push_back(lenang); return true; } return false; } // = {1,2}? // = {1,2}? template // T == css_length or float bool parse_color_stop(const css_token_vector& tokens, vector& color_stops, document_container* container) { if (tokens.empty() || tokens.size() > 3) return false; web_color color; if (!parse_color(tokens[0], color, container)) return false; if (tokens.size() == 1) // { color_stops.emplace_back(color); return true; } else if (tokens.size() == 2) // { T lenang; if (parse_lenang(tokens[1], lenang)) { color_stops.emplace_back(color, lenang); return true; } } else if (tokens.size() == 3) // { T lenang1, lenang2; if (parse_lenang(tokens[1], lenang1) && parse_lenang(tokens[2], lenang2)) { color_stops.emplace_back(color, lenang1); color_stops.emplace_back(color, lenang2); return true; } } return false; } // = , [ ? , ]# template // T == css_length or float bool parse_color_stop_list(const vector& list, gradient& grad, document_container* container) { if (list.size() < 2) // at least two color-stops must be present return false; if (!parse_color_stop(list[0], grad.m_colors, container)) return false; // [ ? , ]# for (size_t i = 1; i < list.size(); i++) { if (parse_color_hint(list[i], grad.m_colors)) { i++; if (i == list.size()) return false; // color-hint not followed by color-stop } if (!parse_color_stop(list[i], grad.m_colors, container)) return false; } return true; } // https://drafts.csswg.org/css-images-4/#linear-gradients // [ | to ] || bool parse_linear_gradient_direction_and_interpolation(const css_token_vector& tokens, gradient& gradient) { float angle = 180; int side = gradient_side_none; auto color_space = color_space_oklab; auto hue_interpolation = hue_interpolation_shorter; int index = 0; if (parse_linear_gradient_direction(tokens, index, angle, side)) { parse_color_interpolation_method(tokens, index, color_space, hue_interpolation); } else if (parse_color_interpolation_method(tokens, index, color_space, hue_interpolation)) { parse_linear_gradient_direction(tokens, index, angle, side); } else return false; if (index != (int)tokens.size()) return false; gradient.angle = angle; gradient.m_side = side; gradient.color_space = color_space; gradient.hue_interpolation = hue_interpolation; return true; } // https://drafts.csswg.org/css-images-4/#linear-gradients // | to // = [left | right] || [top | bottom] bool parse_linear_gradient_direction(const css_token_vector& tokens, int& index, float& angle, int& side) { if (parse_angle(at(tokens, index), angle)) { index++; return true; } if (at(tokens, index).ident() != "to") return false; string a = at(tokens, index + 1).ident(); string b = at(tokens, index + 2).ident(); if (is_one_of(a, "left", "right", "top", "bottom")) { if (!is_one_of(b, "left", "right", "top", "bottom")) { switch (_id(a)) { case _top_: angle = 0; break; case _bottom_: angle = 180; break; case _left_: angle = 270; break; case _right_: angle = 90; break; default: return false; } index += 2; return true; } else { // fix order if (is_one_of(a, "top", "bottom")) swap(a, b); // check order if (!is_one_of(a, "left", "right") || !is_one_of(b, "top", "bottom")) return false; side = a == "left" ? gradient_side_left : gradient_side_right; side |= b == "top" ? gradient_side_top : gradient_side_bottom; index += 3; return true; } } return false; } // https://drafts.csswg.org/css-images-4/#typedef-conic-gradient-syntax // [ from ]? [ at ]? bool parse_conic_angle_position(const css_token_vector& tokens, int& index, gradient& gradient) { if (at(tokens, index).ident() == "from" && parse_angle(at(tokens, index + 1), gradient.conic_from_angle)) index += 2; int i = index; if (at(tokens, i).ident() == "at" && parse_gradient_position(tokens, ++i, gradient)) index = i; return true; } // [ [ from ]? [ at ]? ] || bool parse_conic_gradient_angle_position_interpolation(const css_token_vector& tokens, gradient& gradient) { if (tokens.empty()) return false; auto color_space = color_space_oklab; auto hue_interpolation = hue_interpolation_shorter; int index = 0; // checking color interpolation first because parse_conic_angle_position always succeeds if (parse_color_interpolation_method(tokens, index, color_space, hue_interpolation)) { parse_conic_angle_position(tokens, index, gradient); } else if (parse_conic_angle_position(tokens, index, gradient)) { parse_color_interpolation_method(tokens, index, color_space, hue_interpolation); } else return false; if (index != (int)tokens.size()) return false; gradient.color_space = color_space; gradient.hue_interpolation = hue_interpolation; return true; } const float pi = 3.14159265f; // https://drafts.csswg.org/css-values-4/#angles bool parse_angle(const css_token& tok, float& angle, bool percents_allowed) { // The unit identifier may be omitted if the is zero. https://drafts.csswg.org/css-images-3/#linear-gradient-syntax if (tok.type == NUMBER && tok.n.number == 0) { angle = 0; return true; } // in conic gradient if (tok.type == PERCENTAGE && percents_allowed) { angle = tok.n.number * 360 / 100; return true; } if (tok.type == DIMENSION) { switch (_id(lowcase(tok.unit))) { case _deg_: angle = tok.n.number; break; case _grad_: angle = (tok.n.number / 400) * 360; break; case _rad_: angle = (tok.n.number / (2 * pi)) * 360; break; case _turn_: angle = tok.n.number * 360; break; default: return false; } return true; } return false; } // https://www.w3.org/TR/css-color-4/#color-interpolation-method // = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020 | lab | oklab | xyz | xyz-d50 | xyz-d65 // = hsl | hwb | lch | oklch // = [ shorter | longer | increasing | decreasing ] hue // = in [ | ? ] bool parse_color_interpolation_method(const css_token_vector& tokens, int& index, color_space_t& color_space, hue_interpolation_t& hue_interpolation) { if (at(tokens, index).ident() == "in" && parse_keyword(at(tokens, index + 1), color_space, color_space_strings, 1)) { index += 2; } else return false; if (color_space >= color_space_polar_start && at(tokens, index + 1).ident() == "hue" && // must be checked before parse_keyword, otherwise hue_interpolation may be assigned a value when there is no "hue" keyword parse_keyword(at(tokens, index), hue_interpolation, hue_interpolation_strings, 1)) { index += 2; } return true; } // https://www.w3.org/TR/css-images-3/#typedef-radial-size // = | | {2} // = closest-corner | closest-side | farthest-corner | farthest-side // Permitted values also depend on , see parse_radial_gradient_shape_size_position_interpolation. // TODO: syntax was extended in https://drafts.csswg.org/css-images-4/#radial-size bool parse_radial_size(const css_token_vector& tokens, int& index, gradient& gradient) { auto& tok0 = at(tokens, index); auto& tok1 = at(tokens, index + 1); if (parse_keyword(tok0, gradient.radial_extent, radial_extent_strings, 1)) { index++; return true; } css_length length[2]; if (length[0].from_token(tok0, f_length_percentage | f_positive) && length[1].from_token(tok1, f_length_percentage | f_positive)) { gradient.radial_extent = radial_extent_none; gradient.radial_radius_x = length[0]; gradient.radial_radius_y = length[1]; index += 2; return true; } if (length[0].from_token(tok0, f_length | f_positive)) { gradient.radial_extent = radial_extent_none; gradient.radial_radius_x = length[0]; index++; return true; } return false; } bool parse_gradient_position(const css_token_vector& tokens, int& index, gradient& gradient) { css_length x, y; if (!parse_bg_position(tokens, index, x, y, false)) return false; gradient.m_side = 0; if (x.is_predefined()) { if (x.predef() == background_position_center) gradient.m_side |= gradient_side_x_center; else gradient.m_side |= 1 << x.predef(); } else { gradient.m_side |= gradient_side_x_length; gradient.position_x = x; } if (y.is_predefined()) { if (y.predef() == background_position_center) gradient.m_side |= gradient_side_y_center; else gradient.m_side |= 1 << y.predef(); } else { gradient.m_side |= gradient_side_y_length; gradient.position_y = y; } return true; } // https://drafts.csswg.org/css-images-4/#radial-gradients // [ [ || ]? [ at ]? ] || bool parse_radial_gradient_shape_size_position_interpolation(const css_token_vector& tokens, gradient& result) { // this check is needed because parse may succeed without consuming any input if (tokens.empty()) return false; auto shape = radial_shape_none; auto radial_shape = [&](const css_token_vector& tokens, int& index) { if (!parse_keyword(at(tokens, index), shape, "circle;ellipse", 1)) return false; index++; return true; }; using namespace std::placeholders; gradient grad; // sets grad.radial_extent or grad.radial_radius_{x,y} parse_fn radial_size = std::bind( parse_radial_size, _1, _2, std::ref(grad) ); // sets grad.m_side and grad.radial_position_{x,y} parse_fn radial_position = std::bind( parse_gradient_position, _1, _2, std::ref(grad) ); auto color_space = color_space_oklab; auto hue_interpolation = hue_interpolation_shorter; auto color_interpolation_method = [&](const css_token_vector& tokens, int& index) { return parse_color_interpolation_method(tokens, index, color_space, hue_interpolation); }; ///////////////////////////////////////////////////////////////////////////////////////// auto parse = oror( color_interpolation_method, // first trying this because seq(opt,opt) always succeeds seq(opt(oror(radial_shape, radial_size)), opt(seq("at"_x, radial_position))) ); int index = 0; bool ok = parse(tokens, index) && end(tokens, index); if (!ok) return false; ///////////////////////////////////////////////////////////////////////////////////////// // If is specified as circle or is omitted, the may be given explicitly as if (shape == radial_shape_ellipse && // radius_x is specified, but radius_y is not !grad.radial_radius_x.is_predefined() && grad.radial_radius_y.is_predefined()) return false; // If is specified as ellipse or is omitted, may instead be given explicitly as {2} if (shape == radial_shape_circle && // both radius_x and radius_y are specified !grad.radial_radius_y.is_predefined()) return false; // If is omitted, the ending shape defaults to a circle if the is a single , and to an ellipse otherwise. if (shape == radial_shape_none) { if (!grad.radial_radius_x.is_predefined() && grad.radial_radius_y.is_predefined()) shape = radial_shape_circle; else shape = radial_shape_ellipse; } ///////////////////////////////////////////////////////////////////////////////////////// result.radial_shape = shape; result.radial_extent = grad.radial_extent; result.radial_radius_x = grad.radial_radius_x; result.radial_radius_y = grad.radial_radius_y; result.m_side = grad.m_side; result.position_x = grad.position_x; result.position_y = grad.position_y; result.color_space = color_space; result.hue_interpolation = hue_interpolation; return true; } }