#include "html.h"
#include "media_query.h"
#include "css_parser.h"
#include <cassert>
#include "document.h"
#include "document_container.h"
namespace litehtml
{
bool eval_op(float x, short op, float value)
{
const float epsilon = 0.00001f;
if (abs(x - value) < epsilon)
{
if (is_one_of(op, '=', u'⩾', u'⩽')) return true;
if (op == u'≠') return false;
}
switch (op)
{
case '<': return x < value;
case u'⩽': return x <= value;
case '>': return x > value;
case u'⩾': return x >= value;
case '=': return x == value;
case u'≠': return x != value;
default: return false;
}
}
bool media_feature::compare(float x) const
{
if (!op2) return eval_op(x, op, value);
return eval_op(value, op, x) && eval_op(x, op2, value2);
}
bool media_feature::check(const media_features& feat) const
{
switch (_id(name))
{
case _width_:
return compare(feat.width);
case _height_:
return compare(feat.height);
case _device_width_:
return compare(feat.device_width);
case _device_height_:
return compare(feat.device_height);
case _orientation_:
return compare(feat.height >= feat.width ? _portrait_ : _landscape_);
case _aspect_ratio_:
return feat.height != 0 ? compare(float(feat.width) / feat.height) : false;
case _device_aspect_ratio_:
return feat.device_height != 0 ? compare(float(feat.device_width) / feat.device_height) : false;
case _color_:
return compare(feat.color);
case _color_index_:
return compare(feat.color_index);
case _monochrome_:
return compare(feat.monochrome);
case _resolution_:
return compare(feat.resolution);
default:
assert(0); return false;
}
}
trilean media_condition::check(const media_features& features) const
{
if (op == _not_)
return not m_conditions[0].check(features);
if (op == _and_)
{
trilean result = True;
for (const auto& condition : m_conditions)
{
result = result && condition.check(features);
if (result == False) return result; }
return result;
}
if (op == _or_)
{
trilean result = False;
for (const auto& condition : m_conditions)
{
result = result || condition.check(features);
if (result == True) return result; }
return result;
}
return False;
}
trilean media_in_parens::check(const media_features& features) const
{
if (is<media_condition>()) return get<media_condition>().check(features);
if (is<media_feature>()) return (trilean)get<media_feature>().check(features);
return Unknown;
}
trilean media_query::check(const media_features& features) const
{
trilean result;
if (m_media_type >= media_type_first_deprecated)
result = False;
else if (m_media_type == media_type_unknown)
result = False;
else if (m_media_type == media_type_all)
result = True;
else
result = (trilean)(m_media_type == features.type);
if (result == True)
{
for (const auto& condition : m_conditions)
{
result = result && condition.check(features);
if (result == False) break; }
}
if (m_not) result = not result;
return result;
}
bool media_query_list::check(const media_features& features) const
{
if (empty()) return true;
trilean result = False;
for (const auto& query : m_queries)
{
result = result || query.check(features);
if (result == True) break; }
return result == True;
}
bool media_query_list_list::apply_media_features(const media_features& features)
{
bool apply = true;
for (const auto& mq_list: m_media_query_lists)
{
if (!mq_list.check(features))
{
apply = false;
break;
}
}
bool ret = (apply != m_is_used);
m_is_used = apply;
return ret;
}
bool parse_media_query(const css_token_vector& tokens, media_query& mquery, document::ptr doc);
media_query_list parse_media_query_list(const css_token_vector& _tokens, document::ptr doc)
{
auto keep_whitespace = [](auto left_token, auto right_token)
{
return is_one_of(left_token.ch, '<', '>') && right_token.ch == '=';
};
css_token_vector tokens = normalize(_tokens, f_componentize | f_remove_whitespace, keep_whitespace);
if (tokens.empty()) return {};
media_query_list result;
auto list_of_lists = parse_comma_separated_list(tokens);
for (const auto& list : list_of_lists)
{
media_query query;
parse_media_query(list, query, doc);
result.m_queries.push_back(query);
}
return result;
}
media_query_list parse_media_query_list(const string& str, shared_ptr<document> doc)
{
auto tokens = normalize(str);
return parse_media_query_list(tokens, doc);
}
bool parse_media_condition(const css_token_vector& tokens, int& index, bool or_allowed, media_condition& condition, document::ptr doc);
bool parse_media_query(const css_token_vector& tokens, media_query& mquery, document::ptr doc)
{
if (tokens.empty()) return false;
int index = 0;
auto end = [&]() { return index == (int)tokens.size(); };
media_condition condition;
if (parse_media_condition(tokens, index, true, condition, doc) && end())
{
mquery.m_not = false;
mquery.m_media_type = media_type_all;
mquery.m_conditions = {condition};
return true;
}
string ident = tokens[0].ident();
bool _not = false;
if (ident == "not") index++, _not = true;
else if (ident == "only") index++;
ident = at(tokens, index).ident();
if (is_one_of(ident, "", "only", "not", "and", "or", "layer"))
return false;
int idx = value_index(ident, media_type_strings);
int media_type = idx == -1 ? media_type_unknown : idx + 1;
index++;
if (at(tokens, index).ident() == "and")
{
index++;
if (!parse_media_condition(tokens, index, false, condition, doc) || !end())
return false;
mquery.m_conditions = {condition};
}
if (!end()) return false;
mquery.m_not = _not;
mquery.m_media_type = (litehtml::media_type) media_type;
return true;
}
bool parse_media_in_parens(const css_token& token, media_in_parens& media_in_parens, document::ptr doc);
bool parse_media_condition(const css_token_vector& tokens, int& index, bool _or_allowed, media_condition& condition, document::ptr doc)
{
media_in_parens media_in_parens;
if (at(tokens, index).ident() == "not")
{
if (!parse_media_in_parens(at(tokens, index + 1), media_in_parens, doc)) return false;
condition.op = _not_;
condition.m_conditions = {media_in_parens};
index += 2;
return true;
}
if (!parse_media_in_parens(at(tokens, index), media_in_parens, doc)) return false;
condition.m_conditions = {media_in_parens};
index++;
bool or_allowed = _or_allowed;
bool and_allowed = true;
while (true)
{
string ident = at(tokens, index).ident();
if (ident == "and" && and_allowed) condition.op = _and_, or_allowed = false;
else if (ident == "or" && or_allowed) condition.op = _or_, and_allowed = false;
else return true;
index++;
if (!parse_media_in_parens(at(tokens, index), media_in_parens, doc)) return false;
condition.m_conditions.push_back(media_in_parens);
index++;
}
}
bool parse_media_feature(const css_token& token, media_feature& media_feature, document::ptr doc);
bool parse_media_in_parens(const css_token& token, media_in_parens& media_in_parens, document::ptr doc)
{
if (token.type == CV_FUNCTION)
{
if (!token.value.empty() && !is_any_value(token.value))
return false;
media_in_parens = unknown();
return true;
}
if (token.type != ROUND_BLOCK) return false;
const css_token_vector& tokens = token.value;
int index = 0;
media_condition condition;
media_feature media_feature;
if (parse_media_condition(tokens, index, true, condition, doc) && index == (int)tokens.size())
media_in_parens = condition;
else if (parse_media_feature(token, media_feature, doc))
media_in_parens = media_feature;
else if (!tokens.empty() && !is_any_value(tokens))
return false;
else
media_in_parens = unknown();
return true;
}
bool parse_mf_value(const css_token_vector& tokens, int& index, css_token val[2]);
bool parse_mf_range(const css_token_vector& tokens, media_feature& media_feature, document::ptr doc);
struct mf_info
{
string_id type = empty_id; string_id value_type = empty_id; vector<string_id> keywords = {};
operator bool() { return type != empty_id; }
};
std::map<string, mf_info> supported_media_features =
{
{"width", {_range_, _length_}},
{"height", {_range_, _length_}},
{"aspect-ratio", {_range_, _ratio_}},
{"orientation", {_discrete_, _keyword_, {_portrait_, _landscape_}}},
{"resolution", {_range_, _resolution_}},
{"color", {_range_, _integer_}},
{"color-index", {_range_, _integer_}},
{"monochrome", {_range_, _integer_}},
{"device-width", {_range_, _length_}},
{"device-height", {_range_, _length_}},
{"device-aspect-ratio", {_range_, _ratio_}},
};
bool convert_units(mf_info mfi, css_token val[2], document::ptr doc)
{
switch (mfi.value_type)
{
case _integer_:
return val[0].type == NUMBER && val[0].n.number_type == css_number_integer && val[1].type == 0;
case _length_:
{
if (val[1].type != 0) return false;
css_length length;
if (!length.from_token(val[0], f_length)) return false;
font_metrics fm;
fm.x_height = fm.font_size = doc->container()->get_default_font_size();
doc->cvt_units(length, fm, 0);
val[0].n.number = length.val();
return true;
}
case _resolution_: if (val[1].type != 0) return false;
if (val[0].type == DIMENSION)
{
string unit = lowcase(val[0].unit);
int idx = value_index(unit, "dpi;dpcm;dppx;x"); if (idx < 0 || val[0].n.number < 0) return false;
if (unit == "dppx" || unit == "x")
val[0].n.number *= 96;
else if (unit == "dpcm")
val[0].n.number *= 2.54f; return true;
}
else if (val[0].ident() == "infinite")
{
val[0] = css_token(NUMBER, INFINITY, css_number_number);
return true;
}
return false;
case _ratio_: if (val[0].type == NUMBER && val[0].n.number >= 0 &&
((val[1].type == NUMBER && val[1].n.number >= 0) || val[1].type == 0))
{
if (val[1].type == NUMBER)
val[0].n.number /= val[1].n.number; return true;
}
return false;
case _keyword_:
{
if (val[1].type != 0) return false;
string_id ident = _id(val[0].ident());
if (!contains(mfi.keywords, ident)) return false;
val[0] = css_token(NUMBER, (float)ident);
return true;
}
default:
return false;
}
}
bool media_feature::verify_and_convert_units(string_id syntax,
css_token val[2], css_token val2[2], document::ptr doc)
{
if (syntax == _boolean_) {
auto mf_info = at(supported_media_features, name);
if (!mf_info) return false;
value = mf_info.value_type == _keyword_ ? (float)_none_ : 0;
op = u'≠';
return true;
}
else if (syntax == _plain_) {
if (is_one_of(name.substr(0, 4), "min-", "max-"))
{
string real_name = name.substr(4);
auto mf_info = at(supported_media_features, real_name);
if (!mf_info || mf_info.type == _discrete_)
return false;
if (!convert_units(mf_info, val, doc))
return false;
value = val[0].n.number;
op = name.substr(0, 4) == "min-" ? u'⩾' : u'⩽';
name = real_name;
return true;
}
else
{
auto mf_info = at(supported_media_features, name);
if (!mf_info) return false;
if (!convert_units(mf_info, val, doc))
return false;
value = val[0].n.number;
op = '=';
return true;
}
}
else {
auto mf_info = at(supported_media_features, name);
if (!mf_info || mf_info.type == _discrete_)
return false;
{
if (!convert_units(mf_info, val, doc))
return false;
value = val[0].n.number;
}
if (val2)
{
if (!convert_units(mf_info, val2, doc))
return false;
value2 = val2[0].n.number;
}
return true;
}
}
bool parse_media_feature(const css_token& token, media_feature& result, document::ptr doc)
{
if (token.type != ROUND_BLOCK || token.value.empty()) return false;
const css_token_vector& tokens = token.value;
if (tokens.size() == 1)
{
media_feature mf = {tokens[0].ident()};
if (!mf.verify_and_convert_units(_boolean_)) return false;
result = mf;
return true;
}
if (tokens[0].type == IDENT && tokens[1].ch == ':')
{
css_token val[2];
int index = 2;
if (!parse_mf_value(tokens, index, val) || index != (int)tokens.size())
return false;
media_feature mf = {tokens[0].ident()};
if (!mf.verify_and_convert_units(_plain_, val, nullptr, doc)) return false;
result = mf;
return true;
}
return parse_mf_range(tokens, result, doc);
}
bool parse_mf_value(const css_token_vector& tokens, int& index, css_token val[2])
{
const css_token& a = at(tokens, index);
const css_token& b = at(tokens, index + 1);
const css_token& c = at(tokens, index + 2);
if (!is_one_of(a.type, NUMBER, DIMENSION, IDENT)) return false;
if (a.type == NUMBER && a.n.number >= 0 && b.ch == '/' &&
c.type == NUMBER && c.n.number >= 0)
{
val[0] = a;
val[1] = c;
index += 3;
}
else
{
val[0] = a;
index++;
}
return true;
}
short mirror(short op)
{
if (op == '<') return '>';
if (op == '>') return '<';
if (op == u'⩽') return u'⩾';
if (op == u'⩾') return u'⩽';
return op;
}
bool parse_mf_range(const css_token_vector& tokens, media_feature& result, document::ptr doc)
{
if (tokens.size() < 3) return false;
int index;
string name;
auto mf_name = [&]()
{
if (at(tokens, index).type != IDENT) return false;
name = at(tokens, index).ident();
if (name == "infinite") return false;
index++;
return true;
};
auto mf_value = [&](css_token _val[2])
{
return parse_mf_value(tokens, index, _val);
};
auto mf_lt_gt = [&](char lg, short& _op)
{
const css_token& tok = at(tokens, index);
const css_token& tok1 = at(tokens, index + 1);
if (tok.ch != lg) return false;
if (tok1.ch == '=')
index+=2, _op = lg == '<' ? u'⩽' : u'⩾';
else
index++, _op = lg;
return true;
};
auto mf_lt = [&](short& _op) { return mf_lt_gt('<', _op); };
auto mf_gt = [&](short& _op) { return mf_lt_gt('>', _op); };
auto mf_comparison = [&](short& _op)
{
const css_token& tok = at(tokens, index);
if (tok.ch == '=')
{
index++;
_op = '=';
return true;
}
return mf_lt(_op) || mf_gt(_op);
};
auto start = [&]() { index = 0; return true; };
auto end = [&]() { return index == (int)tokens.size(); };
short op;
css_token val[2];
auto reverse = [](short& _op) { _op = mirror(_op); return true; };
if ((start() && mf_name() && mf_comparison(op) && mf_value(val) && end()) ||
(start() && mf_value(val) && mf_comparison(op) && mf_name() && end() && reverse(op)))
{
media_feature mf = {name};
mf.op = op;
if (!mf.verify_and_convert_units(_range_, val, nullptr, doc)) return false;
result = mf;
return true;
}
short op2;
css_token val2[2];
if ((start() && mf_value(val) && mf_lt(op) && mf_name() && mf_lt(op2) && mf_value(val2) && end()) ||
(start() && mf_value(val) && mf_gt(op) && mf_name() && mf_gt(op2) && mf_value(val2) && end()))
{
media_feature mf = {name};
mf.op = op;
mf.op2 = op2;
if (!mf.verify_and_convert_units(_range_, val, val2, doc)) return false;
result = mf;
return true;
}
return false;
}
}