/* * Copyright (c) 2018-2020, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #define PARSE_VERIFY(x) \ if (!(x)) { \ dbgln("CSS PARSER ASSERTION FAILED: {}", #x); \ dbgln("At character# {} in CSS: _{}_", index, css); \ VERIFY_NOT_REACHED(); \ } static inline void log_parse_error(const SourceLocation& location = SourceLocation::current()) { dbgln("CSS Parse error! {}", location); } namespace Web { namespace CSS { DeprecatedParsingContext::DeprecatedParsingContext() { } DeprecatedParsingContext::DeprecatedParsingContext(const DOM::Document& document) : m_document(&document) { } DeprecatedParsingContext::DeprecatedParsingContext(const DOM::ParentNode& parent_node) : m_document(&parent_node.document()) { } bool DeprecatedParsingContext::in_quirks_mode() const { return m_document ? m_document->in_quirks_mode() : false; } URL DeprecatedParsingContext::complete_url(const String& addr) const { return m_document ? m_document->url().complete_url(addr) : URL::create_with_url_or_path(addr); } } static Optional parse_css_color(const CSS::DeprecatedParsingContext&, const StringView& view) { if (view.equals_ignoring_case("transparent")) return Color::from_rgba(0x00000000); auto color = Color::from_string(view.to_string().to_lowercase()); if (color.has_value()) return color; return {}; } static Optional try_parse_float(const StringView& string) { const char* str = string.characters_without_null_termination(); size_t len = string.length(); size_t weight = 1; int exp_val = 0; float value = 0.0f; float fraction = 0.0f; bool has_sign = false; bool is_negative = false; bool is_fractional = false; bool is_scientific = false; if (str[0] == '-') { is_negative = true; has_sign = true; } if (str[0] == '+') { has_sign = true; } for (size_t i = has_sign; i < len; i++) { // Looks like we're about to start working on the fractional part if (str[i] == '.') { is_fractional = true; continue; } if (str[i] == 'e' || str[i] == 'E') { if (str[i + 1] == '-' || str[i + 1] == '+') exp_val = atoi(str + i + 2); else exp_val = atoi(str + i + 1); is_scientific = true; continue; } if (str[i] < '0' || str[i] > '9' || exp_val != 0) { return {}; continue; } if (is_fractional) { fraction *= 10; fraction += str[i] - '0'; weight *= 10; } else { value = value * 10; value += str[i] - '0'; } } fraction /= weight; value += fraction; if (is_scientific) { bool divide = exp_val < 0; if (divide) exp_val *= -1; for (int i = 0; i < exp_val; i++) { if (divide) value /= 10; else value *= 10; } } return is_negative ? -value : value; } static CSS::Length parse_length(const CSS::DeprecatedParsingContext& context, const StringView& view, bool& is_bad_length) { CSS::Length::Type type = CSS::Length::Type::Undefined; Optional value; if (view.ends_with('%')) { type = CSS::Length::Type::Percentage; value = try_parse_float(view.substring_view(0, view.length() - 1)); } else if (view.ends_with("px", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Px; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("pt", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Pt; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("pc", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Pc; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("mm", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Mm; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("rem", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Rem; value = try_parse_float(view.substring_view(0, view.length() - 3)); } else if (view.ends_with("em", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Em; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("ex", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Ex; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("vw", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Vw; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("vh", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Vh; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("vmax", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Vmax; value = try_parse_float(view.substring_view(0, view.length() - 4)); } else if (view.ends_with("vmin", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Vmin; value = try_parse_float(view.substring_view(0, view.length() - 4)); } else if (view.ends_with("cm", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Cm; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("in", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::In; value = try_parse_float(view.substring_view(0, view.length() - 2)); } else if (view.ends_with("Q", CaseSensitivity::CaseInsensitive)) { type = CSS::Length::Type::Q; value = try_parse_float(view.substring_view(0, view.length() - 1)); } else if (view == "0") { type = CSS::Length::Type::Px; value = 0; } else if (context.in_quirks_mode()) { type = CSS::Length::Type::Px; value = try_parse_float(view); } else { value = try_parse_float(view); if (value.has_value()) is_bad_length = true; } if (!value.has_value()) return {}; return CSS::Length(value.value(), type); } static bool takes_integer_value(CSS::PropertyID property_id) { return property_id == CSS::PropertyID::ZIndex || property_id == CSS::PropertyID::FontWeight || property_id == CSS::PropertyID::Custom; } static StringView parse_custom_property_name(const StringView& value) { if (!value.starts_with("var(") || !value.ends_with(")")) return {}; // FIXME: Allow for fallback auto first_comma_index = value.find(','); auto length = value.length(); auto substring_length = first_comma_index.has_value() ? first_comma_index.value() - 4 - 1 : length - 4 - 1; return value.substring_view(4, substring_length); } RefPtr parse_css_value(const CSS::DeprecatedParsingContext& context, const StringView& string, CSS::PropertyID property_id) { bool is_bad_length = false; if (takes_integer_value(property_id)) { auto integer = string.to_int(); if (integer.has_value()) return CSS::LengthStyleValue::create(CSS::Length::make_px(integer.value())); } auto length = parse_length(context, string, is_bad_length); if (is_bad_length) { auto float_number = try_parse_float(string); if (float_number.has_value()) return CSS::NumericStyleValue::create(float_number.value()); return nullptr; } if (!length.is_undefined()) return CSS::LengthStyleValue::create(length); if (string.equals_ignoring_case("inherit")) return CSS::InheritStyleValue::create(); if (string.equals_ignoring_case("initial")) return CSS::InitialStyleValue::create(); if (string.equals_ignoring_case("auto")) return CSS::LengthStyleValue::create(CSS::Length::make_auto()); if (string.starts_with("var(")) return CSS::CustomStyleValue::create(parse_custom_property_name(string)); auto value_id = CSS::value_id_from_string(string); if (value_id != CSS::ValueID::Invalid) return CSS::IdentifierStyleValue::create(value_id); auto color = parse_css_color(context, string); if (color.has_value()) return CSS::ColorStyleValue::create(color.value()); return CSS::StringStyleValue::create(string); } RefPtr parse_line_width(const CSS::DeprecatedParsingContext& context, const StringView& part) { auto value = parse_css_value(context, part); if (value && value->is_length()) return static_ptr_cast(value); return nullptr; } RefPtr parse_color(const CSS::DeprecatedParsingContext& context, const StringView& part) { auto value = parse_css_value(context, part); if (value && value->is_color()) return static_ptr_cast(value); return nullptr; } RefPtr parse_line_style(const CSS::DeprecatedParsingContext& context, const StringView& part) { auto parsed_value = parse_css_value(context, part); if (!parsed_value || parsed_value->type() != CSS::StyleValue::Type::Identifier) return nullptr; auto value = static_ptr_cast(parsed_value); if (value->id() == CSS::ValueID::Dotted) return value; if (value->id() == CSS::ValueID::Dashed) return value; if (value->id() == CSS::ValueID::Solid) return value; if (value->id() == CSS::ValueID::Double) return value; if (value->id() == CSS::ValueID::Groove) return value; if (value->id() == CSS::ValueID::Ridge) return value; if (value->id() == CSS::ValueID::None) return value; if (value->id() == CSS::ValueID::Hidden) return value; if (value->id() == CSS::ValueID::Inset) return value; if (value->id() == CSS::ValueID::Outset) return value; return nullptr; } class CSSParser { public: CSSParser(const CSS::DeprecatedParsingContext& context, const StringView& input) : m_context(context) , css(input) { } bool next_is(const char* str) const { size_t len = strlen(str); for (size_t i = 0; i < len; ++i) { if (peek(i) != str[i]) return false; } return true; } char peek(size_t offset = 0) const { if ((index + offset) < css.length()) return css[index + offset]; return 0; } bool consume_specific(char ch) { if (peek() != ch) { dbgln("CSSParser: Peeked '{:c}' wanted specific '{:c}'", peek(), ch); } if (!peek()) { log_parse_error(); return false; } if (peek() != ch) { log_parse_error(); ++index; return false; } ++index; return true; } char consume_one() { PARSE_VERIFY(index < css.length()); return css[index++]; }; bool consume_whitespace_or_comments() { size_t original_index = index; bool in_comment = false; for (; index < css.length(); ++index) { char ch = peek(); if (isspace(ch)) continue; if (!in_comment && ch == '/' && peek(1) == '*') { in_comment = true; ++index; continue; } if (in_comment && ch == '*' && peek(1) == '/') { in_comment = false; ++index; continue; } if (in_comment) continue; break; } return original_index != index; } static bool is_valid_selector_char(char ch) { return isalnum(ch) || ch == '-' || ch == '+' || ch == '_' || ch == '(' || ch == ')' || ch == '@'; } static bool is_valid_selector_args_char(char ch) { return is_valid_selector_char(ch) || ch == ' ' || ch == '\t'; } bool is_combinator(char ch) const { return ch == '~' || ch == '>' || ch == '+'; } static StringView capture_selector_args(const String& pseudo_name) { if (const auto start_pos = pseudo_name.find('('); start_pos.has_value()) { const auto start = start_pos.value() + 1; if (const auto end_pos = pseudo_name.find(')', start); end_pos.has_value()) { return pseudo_name.substring_view(start, end_pos.value() - start).trim_whitespace(); } } return {}; } Optional parse_simple_selector() { auto index_at_start = index; if (consume_whitespace_or_comments()) return {}; if (!peek() || peek() == '{' || peek() == ',' || is_combinator(peek())) return {}; CSS::Selector::SimpleSelector simple_selector; if (peek() == '*') { simple_selector.type = CSS::Selector::SimpleSelector::Type::Universal; consume_one(); return simple_selector; } if (peek() == '.') { simple_selector.type = CSS::Selector::SimpleSelector::Type::Class; consume_one(); } else if (peek() == '#') { simple_selector.type = CSS::Selector::SimpleSelector::Type::Id; consume_one(); } else if (isalpha(peek())) { simple_selector.type = CSS::Selector::SimpleSelector::Type::TagName; } else if (peek() == '[') { simple_selector.type = CSS::Selector::SimpleSelector::Type::Attribute; } else if (peek() == ':') { simple_selector.type = CSS::Selector::SimpleSelector::Type::PseudoClass; } else { simple_selector.type = CSS::Selector::SimpleSelector::Type::Universal; } if ((simple_selector.type != CSS::Selector::SimpleSelector::Type::Universal) && (simple_selector.type != CSS::Selector::SimpleSelector::Type::Attribute) && (simple_selector.type != CSS::Selector::SimpleSelector::Type::PseudoClass)) { while (is_valid_selector_char(peek())) buffer.append(consume_one()); PARSE_VERIFY(!buffer.is_empty()); } auto value = String::copy(buffer); if (simple_selector.type == CSS::Selector::SimpleSelector::Type::TagName) { // Some stylesheets use uppercase tag names, so here's a hack to just lowercase them internally. value = value.to_lowercase(); } simple_selector.value = value; buffer.clear(); if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) { CSS::Selector::SimpleSelector::Attribute::MatchType attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute; String attribute_name; String attribute_value; bool in_value = false; consume_specific('['); char expected_end_of_attribute_selector = ']'; while (peek() != expected_end_of_attribute_selector) { char ch = consume_one(); if (ch == '=' || (ch == '~' && peek() == '=')) { if (ch == '=') { attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch; } else if (ch == '~') { consume_one(); attribute_match_type = CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord; } attribute_name = String::copy(buffer); buffer.clear(); in_value = true; consume_whitespace_or_comments(); if (peek() == '\'') { expected_end_of_attribute_selector = '\''; consume_one(); } else if (peek() == '"') { expected_end_of_attribute_selector = '"'; consume_one(); } continue; } // FIXME: This is a hack that will go away when we replace this with a big boy CSS parser. if (ch == '\\') ch = consume_one(); buffer.append(ch); } if (in_value) attribute_value = String::copy(buffer); else attribute_name = String::copy(buffer); buffer.clear(); simple_selector.attribute.match_type = attribute_match_type; simple_selector.attribute.name = attribute_name; simple_selector.attribute.value = attribute_value; if (expected_end_of_attribute_selector != ']') { if (!consume_specific(expected_end_of_attribute_selector)) return {}; } consume_whitespace_or_comments(); if (!consume_specific(']')) return {}; } if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoClass) { // FIXME: Implement pseudo elements. [[maybe_unused]] bool is_pseudo_element = false; consume_one(); if (peek() == ':') { is_pseudo_element = true; consume_one(); } if (next_is("not")) { buffer.append(consume_one()); buffer.append(consume_one()); buffer.append(consume_one()); if (!consume_specific('(')) return {}; buffer.append('('); while (peek() != ')') buffer.append(consume_one()); if (!consume_specific(')')) return {}; buffer.append(')'); } else { int nesting_level = 0; while (true) { const auto ch = peek(); if (ch == '(') ++nesting_level; else if (ch == ')' && nesting_level > 0) --nesting_level; if (nesting_level > 0 ? is_valid_selector_args_char(ch) : is_valid_selector_char(ch)) buffer.append(consume_one()); else break; }; } auto pseudo_name = String::copy(buffer); buffer.clear(); // Ignore for now, otherwise we produce a "false positive" selector // and apply styles to the element itself, not its pseudo element if (is_pseudo_element) return {}; auto& pseudo_class = simple_selector.pseudo_class; if (pseudo_name.equals_ignoring_case("link")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Link; } else if (pseudo_name.equals_ignoring_case("visited")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Visited; } else if (pseudo_name.equals_ignoring_case("active")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Active; } else if (pseudo_name.equals_ignoring_case("hover")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Hover; } else if (pseudo_name.equals_ignoring_case("focus")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Focus; } else if (pseudo_name.equals_ignoring_case("first-child")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::FirstChild; } else if (pseudo_name.equals_ignoring_case("last-child")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::LastChild; } else if (pseudo_name.equals_ignoring_case("only-child")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::OnlyChild; } else if (pseudo_name.equals_ignoring_case("empty")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Empty; } else if (pseudo_name.equals_ignoring_case("root")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Root; } else if (pseudo_name.equals_ignoring_case("first-of-type")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::FirstOfType; } else if (pseudo_name.equals_ignoring_case("last-of-type")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::LastOfType; } else if (pseudo_name.starts_with("nth-child", CaseSensitivity::CaseInsensitive)) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::NthChild; pseudo_class.nth_child_pattern = CSS::Selector::SimpleSelector::NthChildPattern::parse(capture_selector_args(pseudo_name)); } else if (pseudo_name.starts_with("nth-last-child", CaseSensitivity::CaseInsensitive)) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::NthLastChild; pseudo_class.nth_child_pattern = CSS::Selector::SimpleSelector::NthChildPattern::parse(capture_selector_args(pseudo_name)); } else if (pseudo_name.equals_ignoring_case("before")) { simple_selector.pseudo_element = CSS::Selector::SimpleSelector::PseudoElement::Before; } else if (pseudo_name.equals_ignoring_case("after")) { simple_selector.pseudo_element = CSS::Selector::SimpleSelector::PseudoElement::After; } else if (pseudo_name.equals_ignoring_case("disabled")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Disabled; } else if (pseudo_name.equals_ignoring_case("enabled")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Enabled; } else if (pseudo_name.equals_ignoring_case("checked")) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Checked; } else if (pseudo_name.starts_with("not", CaseSensitivity::CaseInsensitive)) { pseudo_class.type = CSS::Selector::SimpleSelector::PseudoClass::Type::Not; auto not_selector = Web::parse_selector(m_context, capture_selector_args(pseudo_name)); if (not_selector) { pseudo_class.not_selector.clear(); pseudo_class.not_selector.append(not_selector.release_nonnull()); } } else { dbgln("Unknown pseudo class: '{}'", pseudo_name); return {}; } } if (index == index_at_start) { // We consumed nothing. return {}; } return simple_selector; } Optional parse_complex_selector() { auto relation = CSS::Selector::ComplexSelector::Relation::Descendant; if (peek() == '{' || peek() == ',') return {}; if (is_combinator(peek())) { switch (peek()) { case '>': relation = CSS::Selector::ComplexSelector::Relation::ImmediateChild; break; case '+': relation = CSS::Selector::ComplexSelector::Relation::AdjacentSibling; break; case '~': relation = CSS::Selector::ComplexSelector::Relation::GeneralSibling; break; } consume_one(); consume_whitespace_or_comments(); } consume_whitespace_or_comments(); Vector simple_selectors; for (;;) { auto component = parse_simple_selector(); if (!component.has_value()) break; simple_selectors.append(component.value()); // If this assert triggers, we're most likely up to no good. PARSE_VERIFY(simple_selectors.size() < 100); } if (simple_selectors.is_empty()) return {}; return CSS::Selector::ComplexSelector { relation, move(simple_selectors) }; } void parse_selector() { Vector complex_selectors; for (;;) { auto index_before = index; auto complex_selector = parse_complex_selector(); if (complex_selector.has_value()) complex_selectors.append(complex_selector.value()); consume_whitespace_or_comments(); if (!peek() || peek() == ',' || peek() == '{') break; // HACK: If we didn't move forward, just let go. if (index == index_before) break; } if (complex_selectors.is_empty()) return; complex_selectors.first().relation = CSS::Selector::ComplexSelector::Relation::None; current_rule.selectors.append(CSS::Selector::create(move(complex_selectors))); } RefPtr parse_individual_selector() { parse_selector(); if (current_rule.selectors.is_empty()) return {}; return current_rule.selectors.last(); } void parse_selector_list() { for (;;) { auto index_before = index; parse_selector(); consume_whitespace_or_comments(); if (peek() == ',') { consume_one(); continue; } if (peek() == '{') break; // HACK: If we didn't move forward, just let go. if (index_before == index) break; } } bool is_valid_property_name_char(char ch) const { return ch && !isspace(ch) && ch != ':'; } bool is_valid_property_value_char(char ch) const { return ch && ch != '!' && ch != ';' && ch != '}'; } bool is_valid_string_quotes_char(char ch) const { return ch == '\'' || ch == '\"'; } struct ValueAndImportant { String value; bool important { false }; }; ValueAndImportant consume_css_value() { buffer.clear(); int paren_nesting_level = 0; bool important = false; for (;;) { char ch = peek(); if (ch == '(') { ++paren_nesting_level; buffer.append(consume_one()); continue; } if (ch == ')') { PARSE_VERIFY(paren_nesting_level > 0); --paren_nesting_level; buffer.append(consume_one()); continue; } if (paren_nesting_level > 0) { buffer.append(consume_one()); continue; } if (next_is("!important")) { consume_specific('!'); consume_specific('i'); consume_specific('m'); consume_specific('p'); consume_specific('o'); consume_specific('r'); consume_specific('t'); consume_specific('a'); consume_specific('n'); consume_specific('t'); important = true; continue; } if (next_is("/*")) { consume_whitespace_or_comments(); continue; } if (!ch) break; if (ch == '\\') { consume_one(); buffer.append(consume_one()); continue; } if (ch == '}') break; if (ch == ';') break; buffer.append(consume_one()); } // Remove trailing whitespace. while (!buffer.is_empty() && isspace(buffer.last())) buffer.take_last(); auto string = String::copy(buffer); buffer.clear(); return { string, important }; } Optional parse_property() { consume_whitespace_or_comments(); if (peek() == ';') { consume_one(); return {}; } if (peek() == '}') return {}; buffer.clear(); while (is_valid_property_name_char(peek())) buffer.append(consume_one()); auto property_name = String::copy(buffer); buffer.clear(); consume_whitespace_or_comments(); if (!consume_specific(':')) return {}; consume_whitespace_or_comments(); auto [property_value, important] = consume_css_value(); consume_whitespace_or_comments(); if (peek() && peek() != '}') { if (!consume_specific(';')) return {}; } auto property_id = CSS::property_id_from_string(property_name); if (property_id == CSS::PropertyID::Invalid && property_name.starts_with("--")) property_id = CSS::PropertyID::Custom; if (property_id == CSS::PropertyID::Invalid && !property_name.starts_with("-")) { dbgln("CSSParser: Unrecognized property '{}'", property_name); } auto value = parse_css_value(m_context, property_value, property_id); if (!value) return {}; if (property_id == CSS::PropertyID::Custom) { return CSS::StyleProperty { property_id, value.release_nonnull(), property_name, important }; } return CSS::StyleProperty { property_id, value.release_nonnull(), {}, important }; } void parse_declaration() { for (;;) { auto property = parse_property(); if (property.has_value()) { auto property_value = property.value(); if (property_value.property_id == CSS::PropertyID::Custom) current_rule.custom_properties.set(property_value.custom_name, property_value); else current_rule.properties.append(property_value); } consume_whitespace_or_comments(); if (!peek() || peek() == '}') break; } } void parse_style_rule() { parse_selector_list(); if (!consume_specific('{')) { log_parse_error(); return; } parse_declaration(); if (!consume_specific('}')) { log_parse_error(); return; } rules.append(CSS::CSSStyleRule::create(move(current_rule.selectors), CSS::CSSStyleDeclaration::create(move(current_rule.properties), move(current_rule.custom_properties)))); } Optional parse_string() { if (!is_valid_string_quotes_char(peek())) { log_parse_error(); return {}; } char end_char = consume_one(); buffer.clear(); while (peek() && peek() != end_char) { if (peek() == '\\') { consume_specific('\\'); if (peek() == 0) break; } buffer.append(consume_one()); } String string_value(String::copy(buffer)); buffer.clear(); if (consume_specific(end_char)) { return { string_value }; } return {}; } Optional parse_url() { if (is_valid_string_quotes_char(peek())) return parse_string(); buffer.clear(); while (peek() && peek() != ')') buffer.append(consume_one()); String url_value(String::copy(buffer)); buffer.clear(); if (peek() == ')') return { url_value }; return {}; } void parse_at_import_rule() { consume_whitespace_or_comments(); Optional imported_address; if (is_valid_string_quotes_char(peek())) { imported_address = parse_string(); } else if (next_is("url")) { consume_specific('u'); consume_specific('r'); consume_specific('l'); consume_whitespace_or_comments(); if (!consume_specific('(')) return; imported_address = parse_url(); if (!consume_specific(')')) return; } else { log_parse_error(); return; } if (imported_address.has_value()) rules.append(CSS::CSSImportRule::create(m_context.complete_url(imported_address.value()))); // FIXME: We ignore possible media query list while (peek() && peek() != ';') consume_one(); consume_specific(';'); } void parse_at_rule() { HashMap at_rules_parsers({ { "@import", &CSSParser::parse_at_import_rule } }); for (const auto& rule_parser_pair : at_rules_parsers) { if (next_is(rule_parser_pair.key.characters())) { for (char c : rule_parser_pair.key) { consume_specific(c); } (this->*(rule_parser_pair.value))(); return; } } // FIXME: We ignore other @-rules completely for now. while (peek() != 0 && peek() != '{') consume_one(); int level = 0; for (;;) { auto ch = consume_one(); if (ch == '{') { ++level; } else if (ch == '}') { --level; if (level == 0) break; } } } void parse_rule() { consume_whitespace_or_comments(); if (!peek()) return; if (peek() == '@') { parse_at_rule(); } else { parse_style_rule(); } consume_whitespace_or_comments(); } RefPtr parse_sheet() { if (peek(0) == (char)0xef && peek(1) == (char)0xbb && peek(2) == (char)0xbf) { // HACK: Skip UTF-8 BOM. index += 3; } while (peek()) { parse_rule(); } return CSS::CSSStyleSheet::create(move(rules)); } RefPtr parse_standalone_declaration() { consume_whitespace_or_comments(); for (;;) { auto property = parse_property(); if (property.has_value()) { auto property_value = property.value(); if (property_value.property_id == CSS::PropertyID::Custom) current_rule.custom_properties.set(property_value.custom_name, property_value); else current_rule.properties.append(property_value); } consume_whitespace_or_comments(); if (!peek()) break; } return CSS::CSSStyleDeclaration::create(move(current_rule.properties), move(current_rule.custom_properties)); } private: CSS::DeprecatedParsingContext m_context; NonnullRefPtrVector rules; struct CurrentRule { NonnullRefPtrVector selectors; Vector properties; HashMap custom_properties; }; CurrentRule current_rule; Vector buffer; size_t index = 0; StringView css; }; RefPtr parse_selector(const CSS::DeprecatedParsingContext& context, const StringView& selector_text) { CSSParser parser(context, selector_text); return parser.parse_individual_selector(); } RefPtr parse_css(const CSS::DeprecatedParsingContext& context, const StringView& css) { if (css.is_empty()) return CSS::CSSStyleSheet::create({}); CSSParser parser(context, css); return parser.parse_sheet(); } RefPtr parse_css_declaration(const CSS::DeprecatedParsingContext& context, const StringView& css) { if (css.is_empty()) return CSS::CSSStyleDeclaration::create({}, {}); CSSParser parser(context, css); return parser.parse_standalone_declaration(); } RefPtr parse_html_length(const DOM::Document& document, const StringView& string) { auto integer = string.to_int(); if (integer.has_value()) return CSS::LengthStyleValue::create(CSS::Length::make_px(integer.value())); return parse_css_value(CSS::DeprecatedParsingContext(document), string); } }