#include "html.h" #include "media_query.h" #include "css_parser.h" #include #define UNE 8800 // u'≠' #define ULE 10877 // u'⩽' #define UGE 10878 // u'⩾' 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 == UNE) return false; } switch (op) { case '<': return x < value; case ULE: return x <= value; case '>': return x > value; case UGE: return x >= value; case '=': return x == value; case UNE: 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_: // The standard calls 1/0 "a degenerate ratio" https://drafts.csswg.org/css-values-4/#ratio-value, // but it doesn't specify exactly how it behaves in this context: https://drafts.csswg.org/mediaqueries-5/#aspect-ratio. // Chrome/Firefox work as expected with 1/0, for example when both width and height are nonzero // (aspect-ratio < 1/0) evaluates to true. But they behave the same for 0/0, which is unexpected // (0/0 is NaN, so any comparisons should evaluate to false). // 0/1 is also degenerate according to the standard. return feat.height ? compare(float(feat.width) / feat.height) : false; case _device_aspect_ratio_: return feat.device_height ? 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); // must never happen, unknown media features are handled in parse_media_feature return false; } } trilean media_condition::check(const media_features& features) const { if (op == _not_) return !m_conditions[0].check(features); if (op == _and_) { // https://drafts.csswg.org/mediaqueries/#evaluating // The result is true if the child term and all of the // children of the child terms are true, false if at least one of these // terms are false, and unknown otherwise. trilean result = True; for (const auto& condition : m_conditions) { result = result && condition.check(features); if (result == False) return result; // no need to check further } return result; } if (op == _or_) { // The result is false if the child term and all of the // children of the child terms are false, true if at least one of these // terms are true, and unknown otherwise. trilean result = False; for (const auto& condition : m_conditions) { result = result || condition.check(features); if (result == True) return result; // no need to check further } return result; } return False; } trilean media_in_parens::check(const media_features& features) const { if (is()) return get().check(features); if (is()) return (trilean)get().check(features); // https://drafts.csswg.org/mediaqueries/#evaluating return Unknown; } trilean media_query::check(const media_features& features) const { trilean result; // https://drafts.csswg.org/mediaqueries/#media-types // User agents must recognize the following media types as valid, but must make them match nothing. 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; // no need to check further } } if (m_not) result = !result; return result; } // https://drafts.csswg.org/mediaqueries/#mq-list bool media_query_list::check(const media_features& features) const { if (empty()) return true; // An empty media query list evaluates to true. trilean result = False; for (const auto& query : m_queries) { result = result || query.check(features); if (result == True) break; // no need to check further } // https://drafts.csswg.org/mediaqueries/#evaluating // If the result ... is used in any context that expects a two-valued boolean, “unknown” must be converted to “false”. return result == True; } // nested @media rules: https://drafts.csswg.org/css-conditional-3/#processing // all of them must be true for style rules to apply 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); // https://drafts.csswg.org/mediaqueries-5/#typedef-media-query-list 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); // this needs special treatment because empty media query list evaluates to true 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); // Note: appending even if media query failed to parse, as per standard. result.m_queries.push_back(query); } return result; } media_query_list parse_media_query_list(const string& str, shared_ptr 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); // = | [ not | only ]? [ and ]? 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++; // ignored https://drafts.csswg.org/mediaqueries-5/#mq-only // = except only, not, and, or, and layer 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); // = | [ * | * ] // = | * // = not 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); // https://drafts.csswg.org/mediaqueries-5/#typedef-media-in-parens // = ( ) | | // = [ ? ) ] | [ ( ? ) ] 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); // https://drafts.csswg.org/mediaqueries/#mq-ranges // Every media feature defines its “type” as either “range” or “discrete” in its definition table. // The only significant difference between the two types is that “range” media features can be evaluated in a range context and accept “min-” and “max-” prefixes on their name. // https://drafts.csswg.org/mediaqueries/#mq-min-max // “Discrete” type properties do not accept “min-” or “max-” prefixes. Adding such a prefix to a “discrete” type media feature simply results in an unknown feature name. // For example, (min-grid: 1) is invalid, because grid is a “discrete” media feature, and so doesn’t accept the prefixes. (Even though the grid media feature appears to be numeric, as it accepts the values 0 and 1.) struct mf_info { string_id type = empty_id; // range, discrete string_id value_type = empty_id; // length, ratio, resolution, integer, keyword vector keywords = {}; // default value is specified here to get rid of gcc warning "missing initializer for member" operator bool() { return type != empty_id; } }; std::map supported_media_features = { //////////////////////////////////////////////// // 4. Viewport/Page Dimensions Media Features //////////////////////////////////////////////// // https://drafts.csswg.org/mediaqueries/#width // For continuous media, this is the width of the viewport. {"width", {_range_, _length_}}, {"height", {_range_, _length_}}, // https://drafts.csswg.org/mediaqueries/#aspect-ratio // width/height {"aspect-ratio", {_range_, _ratio_}}, // https://drafts.csswg.org/mediaqueries/#orientation {"orientation", {_discrete_, _keyword_, {_portrait_, _landscape_}}}, //////////////////////////////////////////////// // 5. Display Quality Media Features //////////////////////////////////////////////// // https://drafts.csswg.org/mediaqueries/#resolution // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/resolution {"resolution", {_range_, _resolution_}}, //////////////////////////////////////////////// // 6. Color Media Features //////////////////////////////////////////////// // https://drafts.csswg.org/mediaqueries/#color {"color", {_range_, _integer_}}, // https://drafts.csswg.org/mediaqueries/#color-index {"color-index", {_range_, _integer_}}, // https://drafts.csswg.org/mediaqueries/#monochrome {"monochrome", {_range_, _integer_}}, //////////////////////////////////////////////// // Deprecated Media Features //////////////////////////////////////////////// // https://drafts.csswg.org/mediaqueries/#device-width {"device-width", {_range_, _length_}}, {"device-height", {_range_, _length_}}, // https://drafts.csswg.org/mediaqueries/#device-aspect-ratio {"device-aspect-ratio", {_range_, _ratio_}}, }; bool convert_units(mf_info mfi, css_token val[2], document::ptr doc) { switch (mfi.value_type) { case _integer_: // nothing to convert, just verify 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_: // https://drafts.csswg.org/css-values-4/#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"); // x == dppx // The allowed range of values always excludes negative values if (idx < 0 || val[0].n.number < 0) return false; // dppx is the canonical unit, but we convert to dpi instead to match document_container::get_media_features // "Note that due to the 1:96 fixed ratio of CSS in to CSS px, 1dppx is equivalent to 96dpi." if (unit == "dppx" || unit == "x") val[0].n.number *= 96; else if (unit == "dpcm") val[0].n.number *= 2.54f; // 1in = 2.54cm return true; } // https://drafts.csswg.org/mediaqueries/#resolution else if (val[0].ident() == "infinite") { val[0] = css_token(NUMBER, INFINITY, css_number_number); return true; } // Note: doesn't allow unitless zero return false; case _ratio_: // https://drafts.csswg.org/css-values-4/#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; // Note: val[1].n.number may be 0, so result may be inf 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) { // https://drafts.csswg.org/mediaqueries/#mq-boolean-context if (syntax == _boolean_) // (name) { // Attempting to evaluate a min/max prefixed media feature in a boolean context is invalid and a syntax error. auto mf_info = at(supported_media_features, name); if (!mf_info) return false; value = mf_info.value_type == _keyword_ ? (float)_none_ : 0; op = UNE; return true; } else if (syntax == _plain_) // ({min-,max-,}name: value) { 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-" ? UGE : ULE; 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 // range syntax { auto mf_info = at(supported_media_features, name); if (!mf_info || mf_info.type == _discrete_) return false; //if (val) { 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 == ULE) return UGE; if (op == UGE) return ULE; 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(); // special case for (infinite = resolution) // resolution is the only range media feature that can accept a keyword 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 == '<' ? ULE : UGE; 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]; // using lambda to avoid warning "assignment within conditional expression" 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; } } // namespace litehtml