#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;
}
}