mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibWeb/CSS: Move rule-conversion code into its own file
This is an attempt to bring the size of Parser.cpp down. No code changes, just moves and some explicit template instantiations now that we're using them from a different file.
This commit is contained in:
parent
f20822e293
commit
d0d5600f47
Notes:
github-actions[bot]
2024-11-01 16:17:31 +00:00
Author: https://github.com/AtkinsSJ Commit: https://github.com/LadybirdBrowser/ladybird/commit/d0d5600f473 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2106
5 changed files with 901 additions and 868 deletions
|
@ -8,6 +8,7 @@ source_set("Parser") {
|
||||||
"MediaParsing.cpp",
|
"MediaParsing.cpp",
|
||||||
"Parser.cpp",
|
"Parser.cpp",
|
||||||
"ParsingContext.cpp",
|
"ParsingContext.cpp",
|
||||||
|
"RuleParsing.cpp",
|
||||||
"SelectorParsing.cpp",
|
"SelectorParsing.cpp",
|
||||||
"Token.cpp",
|
"Token.cpp",
|
||||||
"Tokenizer.cpp",
|
"Tokenizer.cpp",
|
||||||
|
|
|
@ -80,6 +80,7 @@ set(SOURCES
|
||||||
CSS/Parser/MediaParsing.cpp
|
CSS/Parser/MediaParsing.cpp
|
||||||
CSS/Parser/Parser.cpp
|
CSS/Parser/Parser.cpp
|
||||||
CSS/Parser/ParsingContext.cpp
|
CSS/Parser/ParsingContext.cpp
|
||||||
|
CSS/Parser/RuleParsing.cpp
|
||||||
CSS/Parser/SelectorParsing.cpp
|
CSS/Parser/SelectorParsing.cpp
|
||||||
CSS/Parser/Token.cpp
|
CSS/Parser/Token.cpp
|
||||||
CSS/Parser/Tokenizer.cpp
|
CSS/Parser/Tokenizer.cpp
|
||||||
|
|
|
@ -17,21 +17,9 @@
|
||||||
#include <AK/GenericLexer.h>
|
#include <AK/GenericLexer.h>
|
||||||
#include <AK/SourceLocation.h>
|
#include <AK/SourceLocation.h>
|
||||||
#include <AK/TemporaryChange.h>
|
#include <AK/TemporaryChange.h>
|
||||||
#include <LibWeb/CSS/CSSFontFaceRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSImportRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSKeyframeRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSLayerBlockRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSLayerStatementRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSMediaRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSNamespaceRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSNestedDeclarations.h>
|
|
||||||
#include <LibWeb/CSS/CSSPropertyRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSStyleDeclaration.h>
|
#include <LibWeb/CSS/CSSStyleDeclaration.h>
|
||||||
#include <LibWeb/CSS/CSSStyleRule.h>
|
|
||||||
#include <LibWeb/CSS/CSSStyleSheet.h>
|
#include <LibWeb/CSS/CSSStyleSheet.h>
|
||||||
#include <LibWeb/CSS/CSSStyleValue.h>
|
#include <LibWeb/CSS/CSSStyleValue.h>
|
||||||
#include <LibWeb/CSS/CSSSupportsRule.h>
|
|
||||||
#include <LibWeb/CSS/CalculatedOr.h>
|
#include <LibWeb/CSS/CalculatedOr.h>
|
||||||
#include <LibWeb/CSS/EdgeRect.h>
|
#include <LibWeb/CSS/EdgeRect.h>
|
||||||
#include <LibWeb/CSS/MediaList.h>
|
#include <LibWeb/CSS/MediaList.h>
|
||||||
|
@ -201,6 +189,8 @@ RefPtr<Supports> Parser::parse_a_supports(TokenStream<T>& tokens)
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
template RefPtr<Supports> Parser::parse_a_supports(TokenStream<ComponentValue>&);
|
||||||
|
template RefPtr<Supports> Parser::parse_a_supports(TokenStream<Token>&);
|
||||||
|
|
||||||
OwnPtr<Supports::Condition> Parser::parse_supports_condition(TokenStream<ComponentValue>& tokens)
|
OwnPtr<Supports::Condition> Parser::parse_supports_condition(TokenStream<ComponentValue>& tokens)
|
||||||
{
|
{
|
||||||
|
@ -1529,46 +1519,6 @@ RefPtr<CSSStyleValue> Parser::parse_basic_shape_value(TokenStream<ComponentValue
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-cascade-5/#typedef-layer-name
|
|
||||||
// <layer-name> = <ident> [ '.' <ident> ]*
|
|
||||||
|
|
||||||
// "The CSS-wide keywords are reserved for future use, and cause the rule to be invalid at parse time if used as an <ident> in the <layer-name>."
|
|
||||||
auto is_valid_layer_name_part = [](auto& token) {
|
|
||||||
return token.is(Token::Type::Ident) && !is_css_wide_keyword(token.token().ident());
|
|
||||||
};
|
|
||||||
|
|
||||||
auto transaction = tokens.begin_transaction();
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
if (!tokens.has_next_token() && allow_blank_layer_name == AllowBlankLayerName::Yes) {
|
|
||||||
// No name present, just return a blank one
|
|
||||||
return FlyString();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& first_name_token = tokens.consume_a_token();
|
|
||||||
if (!is_valid_layer_name_part(first_name_token))
|
|
||||||
return {};
|
|
||||||
|
|
||||||
StringBuilder builder;
|
|
||||||
builder.append(first_name_token.token().ident());
|
|
||||||
|
|
||||||
while (tokens.has_next_token()) {
|
|
||||||
// Repeatedly parse `'.' <ident>`
|
|
||||||
if (!tokens.next_token().is_delim('.'))
|
|
||||||
break;
|
|
||||||
tokens.discard_a_token(); // '.'
|
|
||||||
|
|
||||||
auto& name_token = tokens.consume_a_token();
|
|
||||||
if (!is_valid_layer_name_part(name_token))
|
|
||||||
return {};
|
|
||||||
builder.appendff(".{}", name_token.token().ident());
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.commit();
|
|
||||||
return builder.to_fly_string_without_validation();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::is_valid_in_the_current_context(Declaration&)
|
bool Parser::is_valid_in_the_current_context(Declaration&)
|
||||||
{
|
{
|
||||||
// FIXME: Implement this check
|
// FIXME: Implement this check
|
||||||
|
@ -1586,526 +1536,6 @@ bool Parser::is_valid_in_the_current_context(QualifiedRule&)
|
||||||
// FIXME: Implement this check
|
// FIXME: Implement this check
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
JS::GCPtr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
|
|
||||||
{
|
|
||||||
return rule.visit(
|
|
||||||
[this, nested](AtRule const& at_rule) -> JS::GCPtr<CSSRule> {
|
|
||||||
if (has_ignored_vendor_prefix(at_rule.name))
|
|
||||||
return {};
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("font-face"sv))
|
|
||||||
return convert_to_font_face_rule(at_rule);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("import"sv))
|
|
||||||
return convert_to_import_rule(at_rule);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("keyframes"sv))
|
|
||||||
return convert_to_keyframes_rule(at_rule);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("layer"sv))
|
|
||||||
return convert_to_layer_rule(at_rule, nested);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("media"sv))
|
|
||||||
return convert_to_media_rule(at_rule, nested);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("namespace"sv))
|
|
||||||
return convert_to_namespace_rule(at_rule);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("supports"sv))
|
|
||||||
return convert_to_supports_rule(at_rule, nested);
|
|
||||||
|
|
||||||
if (at_rule.name.equals_ignoring_ascii_case("property"sv))
|
|
||||||
return convert_to_property_rule(at_rule);
|
|
||||||
|
|
||||||
// FIXME: More at rules!
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", at_rule.name);
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
[this, nested](QualifiedRule const& qualified_rule) -> JS::GCPtr<CSSRule> {
|
|
||||||
return convert_to_style_rule(qualified_rule, nested);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
|
|
||||||
{
|
|
||||||
TokenStream prelude_stream { qualified_rule.prelude };
|
|
||||||
|
|
||||||
auto maybe_selectors = parse_a_selector_list(prelude_stream,
|
|
||||||
nested == Nested::Yes ? SelectorType::Relative : SelectorType::Standalone);
|
|
||||||
|
|
||||||
if (maybe_selectors.is_error()) {
|
|
||||||
if (maybe_selectors.error() == ParseError::SyntaxError) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
|
|
||||||
if constexpr (CSS_PARSER_DEBUG) {
|
|
||||||
prelude_stream.dump_all_tokens();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybe_selectors.value().is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectorList selectors = maybe_selectors.release_value();
|
|
||||||
if (nested == Nested::Yes) {
|
|
||||||
// "Nested style rules differ from non-nested rules in the following ways:
|
|
||||||
// - A nested style rule accepts a <relative-selector-list> as its prelude (rather than just a <selector-list>).
|
|
||||||
// Any relative selectors are relative to the elements represented by the nesting selector.
|
|
||||||
// - If a selector in the <relative-selector-list> does not start with a combinator but does contain the nesting
|
|
||||||
// selector, it is interpreted as a non-relative selector."
|
|
||||||
// https://drafts.csswg.org/css-nesting-1/#syntax
|
|
||||||
// NOTE: We already parsed the selectors as a <relative-selector-list>
|
|
||||||
|
|
||||||
// Nested relative selectors get a `&` inserted at the beginning.
|
|
||||||
// This is, handily, how the spec wants them serialized:
|
|
||||||
// "When serializing a relative selector in a nested style rule, the selector must be absolutized,
|
|
||||||
// with the implied nesting selector inserted."
|
|
||||||
// - https://drafts.csswg.org/css-nesting-1/#cssom
|
|
||||||
|
|
||||||
SelectorList new_list;
|
|
||||||
new_list.ensure_capacity(selectors.size());
|
|
||||||
for (auto const& selector : selectors) {
|
|
||||||
auto first_combinator = selector->compound_selectors().first().combinator;
|
|
||||||
if (!first_is_one_of(first_combinator, Selector::Combinator::None, Selector::Combinator::Descendant)
|
|
||||||
|| !selector->contains_the_nesting_selector()) {
|
|
||||||
new_list.append(selector->relative_to(Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::Nesting }));
|
|
||||||
} else if (first_combinator == Selector::Combinator::Descendant) {
|
|
||||||
// Replace leading descendant combinator (whitespace) with none, because we're not actually relative.
|
|
||||||
auto copied_compound_selectors = selector->compound_selectors();
|
|
||||||
copied_compound_selectors.first().combinator = Selector::Combinator::None;
|
|
||||||
new_list.append(Selector::create(move(copied_compound_selectors)));
|
|
||||||
} else {
|
|
||||||
new_list.append(selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectors = move(new_list);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
|
|
||||||
if (!declaration) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
|
||||||
for (auto& child : qualified_rule.child_rules) {
|
|
||||||
child.visit(
|
|
||||||
[&](Rule const& rule) {
|
|
||||||
// "In addition to nested style rules, this specification allows nested group rules inside of style rules:
|
|
||||||
// any at-rule whose body contains style rules can be nested inside of a style rule as well."
|
|
||||||
// https://drafts.csswg.org/css-nesting-1/#nested-group-rules
|
|
||||||
if (auto converted_rule = convert_to_rule(rule, Nested::Yes)) {
|
|
||||||
if (is<CSSGroupingRule>(*converted_rule)) {
|
|
||||||
child_rules.append(converted_rule);
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested {} is not allowed inside style rule; discarding.", converted_rule->class_name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[&](Vector<Declaration> const& declarations) {
|
|
||||||
auto* declaration = convert_to_style_declaration(declarations);
|
|
||||||
if (!declaration) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
auto nested_rules = CSSRuleList::create(m_context.realm(), move(child_rules));
|
|
||||||
return CSSStyleRule::create(m_context.realm(), move(selectors), *declaration, *nested_rules);
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-cascade-5/#at-import
|
|
||||||
// @import [ <url> | <string> ]
|
|
||||||
// [ layer | layer(<layer-name>) ]?
|
|
||||||
// <import-conditions> ;
|
|
||||||
//
|
|
||||||
// <import-conditions> = [ supports( [ <supports-condition> | <declaration> ] ) ]?
|
|
||||||
// <media-query-list>?
|
|
||||||
|
|
||||||
if (rule.prelude.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Empty prelude.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Block is not allowed.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
TokenStream tokens { rule.prelude };
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
|
|
||||||
Optional<URL::URL> url = parse_url_function(tokens);
|
|
||||||
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
|
|
||||||
url = m_context.complete_url(tokens.consume_a_token().token().string());
|
|
||||||
|
|
||||||
if (!url.has_value()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
// TODO: Support layers and import-conditions
|
|
||||||
if (tokens.has_next_token()) {
|
|
||||||
if constexpr (CSS_PARSER_DEBUG) {
|
|
||||||
dbgln("Failed to parse @import rule: Trailing tokens after URL are not yet supported.");
|
|
||||||
tokens.dump_all_tokens();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return CSSImportRule::create(url.value(), const_cast<DOM::Document&>(*m_context.document()));
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSRule> Parser::convert_to_layer_rule(AtRule const& rule, Nested nested)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-cascade-5/#at-layer
|
|
||||||
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
||||||
// CSSLayerBlockRule
|
|
||||||
// @layer <layer-name>? {
|
|
||||||
// <rule-list>
|
|
||||||
// }
|
|
||||||
|
|
||||||
// First, the name
|
|
||||||
FlyString layer_name = {};
|
|
||||||
auto prelude_tokens = TokenStream { rule.prelude };
|
|
||||||
if (auto maybe_name = parse_layer_name(prelude_tokens, AllowBlankLayerName::Yes); maybe_name.has_value()) {
|
|
||||||
layer_name = maybe_name.release_value();
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (not a valid layer name) prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
prelude_tokens.discard_whitespace();
|
|
||||||
if (prelude_tokens.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (tokens after layer name) prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then the rules
|
|
||||||
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
|
||||||
rule.for_each_as_rule_list([&](auto& rule) {
|
|
||||||
if (auto child_rule = convert_to_rule(rule, nested))
|
|
||||||
child_rules.append(child_rule);
|
|
||||||
});
|
|
||||||
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
|
|
||||||
return CSSLayerBlockRule::create(m_context.realm(), layer_name, rule_list);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSSLayerStatementRule
|
|
||||||
// @layer <layer-name>#;
|
|
||||||
auto tokens = TokenStream { rule.prelude };
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
Vector<FlyString> layer_names;
|
|
||||||
while (tokens.has_next_token()) {
|
|
||||||
// Comma
|
|
||||||
if (!layer_names.is_empty()) {
|
|
||||||
if (auto comma = tokens.consume_a_token(); !comma.is(Token::Type::Comma)) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer missing separating comma, ({}) prelude = {}; discarding.", comma.to_debug_string(), rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto name = parse_layer_name(tokens, AllowBlankLayerName::No); name.has_value()) {
|
|
||||||
layer_names.append(name.release_value());
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer contains invalid name, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layer_names.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer statement has no layer names, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return CSSLayerStatementRule::create(m_context.realm(), move(layer_names));
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSKeyframesRule> Parser::convert_to_keyframes_rule(AtRule const& rule)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-animations/#keyframes
|
|
||||||
// @keyframes = @keyframes <keyframes-name> { <qualified-rule-list> }
|
|
||||||
// <keyframes-name> = <custom-ident> | <string>
|
|
||||||
// <keyframe-block> = <keyframe-selector># { <declaration-list> }
|
|
||||||
// <keyframe-selector> = from | to | <percentage [0,100]>
|
|
||||||
|
|
||||||
if (rule.prelude.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @keyframes rule: Empty prelude.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Is there some way of detecting if there is a block or not?
|
|
||||||
|
|
||||||
auto prelude_stream = TokenStream { rule.prelude };
|
|
||||||
prelude_stream.discard_whitespace();
|
|
||||||
auto& token = prelude_stream.consume_a_token();
|
|
||||||
if (!token.is_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto name_token = token.token();
|
|
||||||
prelude_stream.discard_whitespace();
|
|
||||||
|
|
||||||
if (prelude_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name_token.is(Token::Type::Ident) && (is_css_wide_keyword(name_token.ident()) || name_token.ident().equals_ignoring_ascii_case("none"sv))) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.ident());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.to_debug_string());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto name = name_token.to_string();
|
|
||||||
|
|
||||||
JS::MarkedVector<CSSRule*> keyframes(m_context.realm().heap());
|
|
||||||
rule.for_each_as_qualified_rule_list([&](auto& qualified_rule) {
|
|
||||||
if (!qualified_rule.child_rules.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes keyframe rule contains at-rules; discarding them.");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto selectors = Vector<CSS::Percentage> {};
|
|
||||||
TokenStream child_tokens { qualified_rule.prelude };
|
|
||||||
while (child_tokens.has_next_token()) {
|
|
||||||
child_tokens.discard_whitespace();
|
|
||||||
if (!child_tokens.has_next_token())
|
|
||||||
break;
|
|
||||||
auto tok = child_tokens.consume_a_token();
|
|
||||||
if (!tok.is_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid selector: {}; discarding.", tok.to_debug_string());
|
|
||||||
child_tokens.reconsume_current_input_token();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
auto token = tok.token();
|
|
||||||
auto read_a_selector = false;
|
|
||||||
if (token.is(Token::Type::Ident)) {
|
|
||||||
if (token.ident().equals_ignoring_ascii_case("from"sv)) {
|
|
||||||
selectors.append(CSS::Percentage(0));
|
|
||||||
read_a_selector = true;
|
|
||||||
}
|
|
||||||
if (token.ident().equals_ignoring_ascii_case("to"sv)) {
|
|
||||||
selectors.append(CSS::Percentage(100));
|
|
||||||
read_a_selector = true;
|
|
||||||
}
|
|
||||||
} else if (token.is(Token::Type::Percentage)) {
|
|
||||||
selectors.append(CSS::Percentage(token.percentage()));
|
|
||||||
read_a_selector = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (read_a_selector) {
|
|
||||||
child_tokens.discard_whitespace();
|
|
||||||
if (child_tokens.consume_a_token().is(Token::Type::Comma))
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
child_tokens.reconsume_current_input_token();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertiesAndCustomProperties properties;
|
|
||||||
qualified_rule.for_each_as_declaration_list([&](auto const& declaration) {
|
|
||||||
extract_property(declaration, properties);
|
|
||||||
});
|
|
||||||
auto style = PropertyOwningCSSStyleDeclaration::create(m_context.realm(), move(properties.properties), move(properties.custom_properties));
|
|
||||||
for (auto& selector : selectors) {
|
|
||||||
auto keyframe_rule = CSSKeyframeRule::create(m_context.realm(), selector, *style);
|
|
||||||
keyframes.append(keyframe_rule);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return CSSKeyframesRule::create(m_context.realm(), name, CSSRuleList::create(m_context.realm(), move(keyframes)));
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSNamespaceRule> Parser::convert_to_namespace_rule(AtRule const& rule)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-namespaces/#syntax
|
|
||||||
// @namespace <namespace-prefix>? [ <string> | <url> ] ;
|
|
||||||
// <namespace-prefix> = <ident>
|
|
||||||
|
|
||||||
if (rule.prelude.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Empty prelude.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Block is not allowed.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto tokens = TokenStream { rule.prelude };
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
|
|
||||||
Optional<FlyString> prefix = {};
|
|
||||||
if (tokens.next_token().is(Token::Type::Ident)) {
|
|
||||||
prefix = tokens.consume_a_token().token().ident();
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
}
|
|
||||||
|
|
||||||
FlyString namespace_uri;
|
|
||||||
if (auto url = parse_url_function(tokens); url.has_value()) {
|
|
||||||
namespace_uri = MUST(url.value().to_string());
|
|
||||||
} else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) {
|
|
||||||
namespace_uri = url_token.token().string();
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
if (tokens.has_next_token()) {
|
|
||||||
if constexpr (CSS_PARSER_DEBUG) {
|
|
||||||
dbgln("Failed to parse @namespace rule: Trailing tokens after URL.");
|
|
||||||
tokens.dump_all_tokens();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return CSSNamespaceRule::create(m_context.realm(), prefix, namespace_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::GCPtr<CSSSupportsRule> Parser::convert_to_supports_rule(AtRule const& rule, Nested nested)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-conditional-3/#at-supports
|
|
||||||
// @supports <supports-condition> {
|
|
||||||
// <rule-list>
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (rule.prelude.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @supports rule: Empty prelude.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto supports_tokens = TokenStream { rule.prelude };
|
|
||||||
auto supports = parse_a_supports(supports_tokens);
|
|
||||||
if (!supports) {
|
|
||||||
if constexpr (CSS_PARSER_DEBUG) {
|
|
||||||
dbgln("Failed to parse @supports rule: supports clause invalid.");
|
|
||||||
supports_tokens.dump_all_tokens();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
|
||||||
rule.for_each_as_rule_list([&](auto& rule) {
|
|
||||||
if (auto child_rule = convert_to_rule(rule, nested))
|
|
||||||
child_rules.append(child_rule);
|
|
||||||
});
|
|
||||||
|
|
||||||
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
|
|
||||||
return CSSSupportsRule::create(m_context.realm(), supports.release_nonnull(), rule_list);
|
|
||||||
}
|
|
||||||
JS::GCPtr<CSSPropertyRule> Parser::convert_to_property_rule(AtRule const& rule)
|
|
||||||
{
|
|
||||||
// https://drafts.css-houdini.org/css-properties-values-api-1/#at-ruledef-property
|
|
||||||
// @property <custom-property-name> {
|
|
||||||
// <declaration-list>
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (rule.prelude.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @property rule: Empty prelude.");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto prelude_stream = TokenStream { rule.prelude };
|
|
||||||
prelude_stream.discard_whitespace();
|
|
||||||
auto const& token = prelude_stream.consume_a_token();
|
|
||||||
if (!token.is_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto name_token = token.token();
|
|
||||||
prelude_stream.discard_whitespace();
|
|
||||||
|
|
||||||
if (prelude_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name_token.is(Token::Type::Ident)) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name is invalid: {}; discarding.", name_token.to_debug_string());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_a_custom_property_name_string(name_token.ident())) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name doesn't start with '--': {}; discarding.", name_token.ident());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto const& name = name_token.ident();
|
|
||||||
|
|
||||||
Optional<FlyString> syntax_maybe;
|
|
||||||
Optional<bool> inherits_maybe;
|
|
||||||
Optional<String> initial_value_maybe;
|
|
||||||
|
|
||||||
rule.for_each_as_declaration_list([&](auto& declaration) {
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("syntax"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
|
|
||||||
auto const& syntax_token = token_stream.consume_a_token();
|
|
||||||
if (syntax_token.is(Token::Type::String)) {
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
if (token_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in syntax");
|
|
||||||
} else {
|
|
||||||
syntax_maybe = syntax_token.token().string();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected value for @property \"syntax\": {}; discarding.", declaration.to_string());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("inherits"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
|
|
||||||
auto const& inherits_token = token_stream.consume_a_token();
|
|
||||||
if (inherits_token.is_ident("true"sv) || inherits_token.is_ident("false"sv)) {
|
|
||||||
auto const& ident = inherits_token.token().ident();
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
if (token_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in inherits");
|
|
||||||
} else {
|
|
||||||
inherits_maybe = (ident == "true");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expected true/false for @property \"inherits\" value, got: {}; discarding.", inherits_token.to_debug_string());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("initial-value"sv)) {
|
|
||||||
// FIXME: Ensure that the initial value matches the syntax, and parse the correct CSSValue out
|
|
||||||
StringBuilder initial_value_sb;
|
|
||||||
for (auto const& component : declaration.value) {
|
|
||||||
initial_value_sb.append(component.to_string());
|
|
||||||
}
|
|
||||||
initial_value_maybe = MUST(initial_value_sb.to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (syntax_maybe.has_value() && inherits_maybe.has_value()) {
|
|
||||||
return CSSPropertyRule::create(m_context.realm(), name, syntax_maybe.value(), inherits_maybe.value(), std::move(initial_value_maybe));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Parser::PropertiesAndCustomProperties Parser::extract_properties(Vector<RuleOrListOfDeclarations> const& rules_and_lists_of_declarations)
|
Parser::PropertiesAndCustomProperties Parser::extract_properties(Vector<RuleOrListOfDeclarations> const& rules_and_lists_of_declarations)
|
||||||
{
|
{
|
||||||
PropertiesAndCustomProperties result;
|
PropertiesAndCustomProperties result;
|
||||||
|
@ -5938,7 +5368,7 @@ RefPtr<CSSStyleValue> Parser::parse_flex_flow_value(TokenStream<ComponentValue>&
|
||||||
{ flex_direction.release_nonnull(), flex_wrap.release_nonnull() });
|
{ flex_direction.release_nonnull(), flex_wrap.release_nonnull() });
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_generic_font_family(Keyword keyword)
|
bool Parser::is_generic_font_family(Keyword keyword)
|
||||||
{
|
{
|
||||||
switch (keyword) {
|
switch (keyword) {
|
||||||
case Keyword::Cursive:
|
case Keyword::Cursive:
|
||||||
|
@ -6295,301 +5725,6 @@ RefPtr<CSSStyleValue> Parser::parse_font_variation_settings_value(TokenStream<Co
|
||||||
return StyleValueList::create(move(axis_tags), StyleValueList::Separator::Comma);
|
return StyleValueList::create(move(axis_tags), StyleValueList::Separator::Comma);
|
||||||
}
|
}
|
||||||
|
|
||||||
JS::GCPtr<CSSFontFaceRule> Parser::convert_to_font_face_rule(AtRule const& rule)
|
|
||||||
{
|
|
||||||
// https://drafts.csswg.org/css-fonts/#font-face-rule
|
|
||||||
|
|
||||||
Optional<FlyString> font_family;
|
|
||||||
Optional<FlyString> font_named_instance;
|
|
||||||
Vector<ParsedFontFace::Source> src;
|
|
||||||
Vector<Gfx::UnicodeRange> unicode_range;
|
|
||||||
Optional<int> weight;
|
|
||||||
Optional<int> slope;
|
|
||||||
Optional<int> width;
|
|
||||||
Optional<Percentage> ascent_override;
|
|
||||||
Optional<Percentage> descent_override;
|
|
||||||
Optional<Percentage> line_gap_override;
|
|
||||||
FontDisplay font_display = FontDisplay::Auto;
|
|
||||||
Optional<FlyString> language_override;
|
|
||||||
Optional<OrderedHashMap<FlyString, i64>> font_feature_settings;
|
|
||||||
Optional<OrderedHashMap<FlyString, double>> font_variation_settings;
|
|
||||||
|
|
||||||
// "normal" is returned as nullptr
|
|
||||||
auto parse_as_percentage_or_normal = [&](Vector<ComponentValue> const& values) -> ErrorOr<Optional<Percentage>> {
|
|
||||||
// normal | <percentage [0,∞]>
|
|
||||||
TokenStream tokens { values };
|
|
||||||
if (auto percentage_value = parse_percentage_value(tokens)) {
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
if (tokens.has_next_token())
|
|
||||||
return Error::from_string_literal("Unexpected trailing tokens");
|
|
||||||
|
|
||||||
if (percentage_value->is_percentage() && percentage_value->as_percentage().percentage().value() >= 0)
|
|
||||||
return percentage_value->as_percentage().percentage();
|
|
||||||
|
|
||||||
// TODO: Once we implement calc-simplification in the parser, we should no longer see math values here,
|
|
||||||
// unless they're impossible to resolve and thus invalid.
|
|
||||||
if (percentage_value->is_math()) {
|
|
||||||
if (auto result = percentage_value->as_math().resolve_percentage(); result.has_value())
|
|
||||||
return result.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Error::from_string_literal("Invalid percentage");
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
if (!tokens.consume_a_token().is_ident("normal"sv))
|
|
||||||
return Error::from_string_literal("Expected `normal | <percentage [0,∞]>`");
|
|
||||||
tokens.discard_whitespace();
|
|
||||||
if (tokens.has_next_token())
|
|
||||||
return Error::from_string_literal("Unexpected trailing tokens");
|
|
||||||
|
|
||||||
return OptionalNone {};
|
|
||||||
};
|
|
||||||
|
|
||||||
rule.for_each_as_declaration_list([&](auto& declaration) {
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("ascent-override"sv)) {
|
|
||||||
auto value = parse_as_percentage_or_normal(declaration.value);
|
|
||||||
if (value.is_error()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face ascent-override: {}", value.error());
|
|
||||||
} else {
|
|
||||||
ascent_override = value.release_value();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("descent-override"sv)) {
|
|
||||||
auto value = parse_as_percentage_or_normal(declaration.value);
|
|
||||||
if (value.is_error()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face descent-override: {}", value.error());
|
|
||||||
} else {
|
|
||||||
descent_override = value.release_value();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-display"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto keyword_value = parse_keyword_value(token_stream)) {
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
if (token_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-display");
|
|
||||||
} else {
|
|
||||||
auto value = keyword_to_font_display(keyword_value->to_keyword());
|
|
||||||
if (value.has_value()) {
|
|
||||||
font_display = *value;
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: `{}` is not a valid value for font-display", keyword_value->to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-family"sv)) {
|
|
||||||
// FIXME: This is very similar to, but different from, the logic in parse_font_family_value().
|
|
||||||
// Ideally they could share code.
|
|
||||||
Vector<FlyString> font_family_parts;
|
|
||||||
bool had_syntax_error = false;
|
|
||||||
for (size_t i = 0; i < declaration.value.size(); ++i) {
|
|
||||||
auto const& part = declaration.value[i];
|
|
||||||
if (part.is(Token::Type::Whitespace))
|
|
||||||
continue;
|
|
||||||
if (part.is(Token::Type::String)) {
|
|
||||||
if (!font_family_parts.is_empty()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
|
||||||
had_syntax_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
font_family_parts.append(part.token().string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (part.is(Token::Type::Ident)) {
|
|
||||||
if (is_css_wide_keyword(part.token().ident())) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
|
||||||
had_syntax_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
auto keyword = keyword_from_string(part.token().ident());
|
|
||||||
if (keyword.has_value() && is_generic_font_family(keyword.value())) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
|
||||||
had_syntax_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
font_family_parts.append(part.token().ident());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
|
||||||
had_syntax_error = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (had_syntax_error || font_family_parts.is_empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
font_family = String::join(' ', font_family_parts).release_value_but_fixme_should_propagate_errors();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-feature-settings"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto value = parse_css_value(CSS::PropertyID::FontFeatureSettings, token_stream); !value.is_error()) {
|
|
||||||
if (value.value()->to_keyword() == Keyword::Normal) {
|
|
||||||
font_feature_settings.clear();
|
|
||||||
} else if (value.value()->is_value_list()) {
|
|
||||||
auto const& feature_tags = value.value()->as_value_list().values();
|
|
||||||
OrderedHashMap<FlyString, i64> settings;
|
|
||||||
settings.ensure_capacity(feature_tags.size());
|
|
||||||
for (auto const& feature_tag : feature_tags) {
|
|
||||||
if (!feature_tag->is_open_type_tagged()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto const& setting_value = feature_tag->as_open_type_tagged().value();
|
|
||||||
if (setting_value->is_integer()) {
|
|
||||||
settings.set(feature_tag->as_open_type_tagged().tag(), setting_value->as_integer().integer());
|
|
||||||
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
|
|
||||||
if (auto integer = setting_value->as_math().resolve_integer(); integer.has_value()) {
|
|
||||||
settings.set(feature_tag->as_open_type_tagged().tag(), *integer);
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-feature-settings descriptor cannot be resolved at parse time; skipping");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue holding a <integer>; skipping");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font_feature_settings = move(settings);
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-feature-settings descriptor, not compatible with value returned from parsing font-feature-settings property: {}", value.value()->to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-language-override"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto maybe_value = parse_css_value(CSS::PropertyID::FontLanguageOverride, token_stream); !maybe_value.is_error()) {
|
|
||||||
auto& value = maybe_value.value();
|
|
||||||
if (value->is_string()) {
|
|
||||||
language_override = value->as_string().string_value();
|
|
||||||
} else {
|
|
||||||
language_override.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-named-instance"sv)) {
|
|
||||||
// auto | <string>
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
auto& token = token_stream.consume_a_token();
|
|
||||||
token_stream.discard_whitespace();
|
|
||||||
if (token_stream.has_next_token()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-named-instance");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token.is_ident("auto"sv)) {
|
|
||||||
font_named_instance.clear();
|
|
||||||
} else if (token.is(Token::Type::String)) {
|
|
||||||
font_named_instance = token.token().string();
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-named-instance from {}", token.to_debug_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-style"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto value = parse_css_value(CSS::PropertyID::FontStyle, token_stream); !value.is_error()) {
|
|
||||||
slope = value.value()->to_font_slope();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-variation-settings"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto value = parse_css_value(CSS::PropertyID::FontVariationSettings, token_stream); !value.is_error()) {
|
|
||||||
if (value.value()->to_keyword() == Keyword::Normal) {
|
|
||||||
font_variation_settings.clear();
|
|
||||||
} else if (value.value()->is_value_list()) {
|
|
||||||
auto const& variation_tags = value.value()->as_value_list().values();
|
|
||||||
OrderedHashMap<FlyString, double> settings;
|
|
||||||
settings.ensure_capacity(variation_tags.size());
|
|
||||||
for (auto const& variation_tag : variation_tags) {
|
|
||||||
if (!variation_tag->is_open_type_tagged()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto const& setting_value = variation_tag->as_open_type_tagged().value();
|
|
||||||
if (setting_value->is_number()) {
|
|
||||||
settings.set(variation_tag->as_open_type_tagged().tag(), setting_value->as_number().number());
|
|
||||||
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
|
|
||||||
if (auto number = setting_value->as_math().resolve_number(); number.has_value()) {
|
|
||||||
settings.set(variation_tag->as_open_type_tagged().tag(), *number);
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-variation-settings descriptor cannot be resolved at parse time; skipping");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue holding a <number>; skipping");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font_variation_settings = move(settings);
|
|
||||||
} else {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-variation-settings descriptor, not compatible with value returned from parsing font-variation-settings property: {}", value.value()->to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-weight"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto value = parse_css_value(CSS::PropertyID::FontWeight, token_stream); !value.is_error()) {
|
|
||||||
weight = value.value()->to_font_weight();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("font-width"sv)
|
|
||||||
|| declaration.name.equals_ignoring_ascii_case("font-stretch"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
if (auto value = parse_css_value(CSS::PropertyID::FontWidth, token_stream); !value.is_error()) {
|
|
||||||
width = value.value()->to_font_width();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("line-gap-override"sv)) {
|
|
||||||
auto value = parse_as_percentage_or_normal(declaration.value);
|
|
||||||
if (value.is_error()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face line-gap-override: {}", value.error());
|
|
||||||
} else {
|
|
||||||
line_gap_override = value.release_value();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("src"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
Vector<ParsedFontFace::Source> supported_sources = parse_font_face_src(token_stream);
|
|
||||||
if (!supported_sources.is_empty())
|
|
||||||
src = move(supported_sources);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (declaration.name.equals_ignoring_ascii_case("unicode-range"sv)) {
|
|
||||||
TokenStream token_stream { declaration.value };
|
|
||||||
auto unicode_ranges = parse_unicode_ranges(token_stream);
|
|
||||||
if (unicode_ranges.is_empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
unicode_range = move(unicode_ranges);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unrecognized descriptor '{}' in @font-face; discarding.", declaration.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!font_family.has_value()) {
|
|
||||||
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face: no font-family!");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unicode_range.is_empty()) {
|
|
||||||
unicode_range.empend(0x0u, 0x10FFFFu);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CSSFontFaceRule::create(m_context.realm(), ParsedFontFace { font_family.release_value(), move(weight), move(slope), move(width), move(src), move(unicode_range), move(ascent_override), move(descent_override), move(line_gap_override), font_display, move(font_named_instance), move(language_override), move(font_feature_settings), move(font_variation_settings) });
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<ParsedFontFace::Source> Parser::parse_as_font_face_src()
|
Vector<ParsedFontFace::Source> Parser::parse_as_font_face_src()
|
||||||
{
|
{
|
||||||
return parse_font_face_src(m_token_stream);
|
return parse_font_face_src(m_token_stream);
|
||||||
|
@ -6687,6 +5822,8 @@ Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<T>& compo
|
||||||
|
|
||||||
return supported_sources;
|
return supported_sources;
|
||||||
}
|
}
|
||||||
|
template Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<Token>& component_values);
|
||||||
|
template Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<ComponentValue>& component_values);
|
||||||
|
|
||||||
RefPtr<CSSStyleValue> Parser::parse_list_style_value(TokenStream<ComponentValue>& tokens)
|
RefPtr<CSSStyleValue> Parser::parse_list_style_value(TokenStream<ComponentValue>& tokens)
|
||||||
{
|
{
|
||||||
|
|
|
@ -383,6 +383,7 @@ private:
|
||||||
bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector<ComponentValue>& dest);
|
bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector<ComponentValue>& dest);
|
||||||
|
|
||||||
static bool has_ignored_vendor_prefix(StringView);
|
static bool has_ignored_vendor_prefix(StringView);
|
||||||
|
static bool is_generic_font_family(Keyword);
|
||||||
|
|
||||||
struct PropertiesAndCustomProperties {
|
struct PropertiesAndCustomProperties {
|
||||||
Vector<StyleProperty> properties;
|
Vector<StyleProperty> properties;
|
||||||
|
|
893
Userland/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp
Normal file
893
Userland/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp
Normal file
|
@ -0,0 +1,893 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
|
||||||
|
* Copyright (c) 2020-2021, the SerenityOS developers.
|
||||||
|
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
|
||||||
|
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
|
||||||
|
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||||
|
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
|
||||||
|
* Copyright (c) 2024, Tommy van der Vorst <tommy@pixelspark.nl>
|
||||||
|
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
|
||||||
|
* Copyright (c) 2024, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <LibWeb/CSS/CSSFontFaceRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSImportRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSKeyframeRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSLayerBlockRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSLayerStatementRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSMediaRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSNamespaceRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSNestedDeclarations.h>
|
||||||
|
#include <LibWeb/CSS/CSSPropertyRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSStyleRule.h>
|
||||||
|
#include <LibWeb/CSS/CSSSupportsRule.h>
|
||||||
|
#include <LibWeb/CSS/Parser/Parser.h>
|
||||||
|
#include <LibWeb/CSS/PropertyName.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
||||||
|
|
||||||
|
namespace Web::CSS::Parser {
|
||||||
|
|
||||||
|
JS::GCPtr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
|
||||||
|
{
|
||||||
|
return rule.visit(
|
||||||
|
[this, nested](AtRule const& at_rule) -> JS::GCPtr<CSSRule> {
|
||||||
|
if (has_ignored_vendor_prefix(at_rule.name))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("font-face"sv))
|
||||||
|
return convert_to_font_face_rule(at_rule);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("import"sv))
|
||||||
|
return convert_to_import_rule(at_rule);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("keyframes"sv))
|
||||||
|
return convert_to_keyframes_rule(at_rule);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("layer"sv))
|
||||||
|
return convert_to_layer_rule(at_rule, nested);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("media"sv))
|
||||||
|
return convert_to_media_rule(at_rule, nested);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("namespace"sv))
|
||||||
|
return convert_to_namespace_rule(at_rule);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("supports"sv))
|
||||||
|
return convert_to_supports_rule(at_rule, nested);
|
||||||
|
|
||||||
|
if (at_rule.name.equals_ignoring_ascii_case("property"sv))
|
||||||
|
return convert_to_property_rule(at_rule);
|
||||||
|
|
||||||
|
// FIXME: More at rules!
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", at_rule.name);
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
[this, nested](QualifiedRule const& qualified_rule) -> JS::GCPtr<CSSRule> {
|
||||||
|
return convert_to_style_rule(qualified_rule, nested);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
|
||||||
|
{
|
||||||
|
TokenStream prelude_stream { qualified_rule.prelude };
|
||||||
|
|
||||||
|
auto maybe_selectors = parse_a_selector_list(prelude_stream,
|
||||||
|
nested == Nested::Yes ? SelectorType::Relative : SelectorType::Standalone);
|
||||||
|
|
||||||
|
if (maybe_selectors.is_error()) {
|
||||||
|
if (maybe_selectors.error() == ParseError::SyntaxError) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
|
||||||
|
if constexpr (CSS_PARSER_DEBUG) {
|
||||||
|
prelude_stream.dump_all_tokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybe_selectors.value().is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectorList selectors = maybe_selectors.release_value();
|
||||||
|
if (nested == Nested::Yes) {
|
||||||
|
// "Nested style rules differ from non-nested rules in the following ways:
|
||||||
|
// - A nested style rule accepts a <relative-selector-list> as its prelude (rather than just a <selector-list>).
|
||||||
|
// Any relative selectors are relative to the elements represented by the nesting selector.
|
||||||
|
// - If a selector in the <relative-selector-list> does not start with a combinator but does contain the nesting
|
||||||
|
// selector, it is interpreted as a non-relative selector."
|
||||||
|
// https://drafts.csswg.org/css-nesting-1/#syntax
|
||||||
|
// NOTE: We already parsed the selectors as a <relative-selector-list>
|
||||||
|
|
||||||
|
// Nested relative selectors get a `&` inserted at the beginning.
|
||||||
|
// This is, handily, how the spec wants them serialized:
|
||||||
|
// "When serializing a relative selector in a nested style rule, the selector must be absolutized,
|
||||||
|
// with the implied nesting selector inserted."
|
||||||
|
// - https://drafts.csswg.org/css-nesting-1/#cssom
|
||||||
|
|
||||||
|
SelectorList new_list;
|
||||||
|
new_list.ensure_capacity(selectors.size());
|
||||||
|
for (auto const& selector : selectors) {
|
||||||
|
auto first_combinator = selector->compound_selectors().first().combinator;
|
||||||
|
if (!first_is_one_of(first_combinator, Selector::Combinator::None, Selector::Combinator::Descendant)
|
||||||
|
|| !selector->contains_the_nesting_selector()) {
|
||||||
|
new_list.append(selector->relative_to(Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::Nesting }));
|
||||||
|
} else if (first_combinator == Selector::Combinator::Descendant) {
|
||||||
|
// Replace leading descendant combinator (whitespace) with none, because we're not actually relative.
|
||||||
|
auto copied_compound_selectors = selector->compound_selectors();
|
||||||
|
copied_compound_selectors.first().combinator = Selector::Combinator::None;
|
||||||
|
new_list.append(Selector::create(move(copied_compound_selectors)));
|
||||||
|
} else {
|
||||||
|
new_list.append(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectors = move(new_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
|
||||||
|
if (!declaration) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
||||||
|
for (auto& child : qualified_rule.child_rules) {
|
||||||
|
child.visit(
|
||||||
|
[&](Rule const& rule) {
|
||||||
|
// "In addition to nested style rules, this specification allows nested group rules inside of style rules:
|
||||||
|
// any at-rule whose body contains style rules can be nested inside of a style rule as well."
|
||||||
|
// https://drafts.csswg.org/css-nesting-1/#nested-group-rules
|
||||||
|
if (auto converted_rule = convert_to_rule(rule, Nested::Yes)) {
|
||||||
|
if (is<CSSGroupingRule>(*converted_rule)) {
|
||||||
|
child_rules.append(converted_rule);
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested {} is not allowed inside style rule; discarding.", converted_rule->class_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[&](Vector<Declaration> const& declarations) {
|
||||||
|
auto* declaration = convert_to_style_declaration(declarations);
|
||||||
|
if (!declaration) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
auto nested_rules = CSSRuleList::create(m_context.realm(), move(child_rules));
|
||||||
|
return CSSStyleRule::create(m_context.realm(), move(selectors), *declaration, *nested_rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-cascade-5/#at-import
|
||||||
|
// @import [ <url> | <string> ]
|
||||||
|
// [ layer | layer(<layer-name>) ]?
|
||||||
|
// <import-conditions> ;
|
||||||
|
//
|
||||||
|
// <import-conditions> = [ supports( [ <supports-condition> | <declaration> ] ) ]?
|
||||||
|
// <media-query-list>?
|
||||||
|
|
||||||
|
if (rule.prelude.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Empty prelude.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Block is not allowed.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenStream tokens { rule.prelude };
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
|
||||||
|
Optional<URL::URL> url = parse_url_function(tokens);
|
||||||
|
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
|
||||||
|
url = m_context.complete_url(tokens.consume_a_token().token().string());
|
||||||
|
|
||||||
|
if (!url.has_value()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
// TODO: Support layers and import-conditions
|
||||||
|
if (tokens.has_next_token()) {
|
||||||
|
if constexpr (CSS_PARSER_DEBUG) {
|
||||||
|
dbgln("Failed to parse @import rule: Trailing tokens after URL are not yet supported.");
|
||||||
|
tokens.dump_all_tokens();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return CSSImportRule::create(url.value(), const_cast<DOM::Document&>(*m_context.document()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-cascade-5/#typedef-layer-name
|
||||||
|
// <layer-name> = <ident> [ '.' <ident> ]*
|
||||||
|
|
||||||
|
// "The CSS-wide keywords are reserved for future use, and cause the rule to be invalid at parse time if used as an <ident> in the <layer-name>."
|
||||||
|
auto is_valid_layer_name_part = [](auto& token) {
|
||||||
|
return token.is(Token::Type::Ident) && !is_css_wide_keyword(token.token().ident());
|
||||||
|
};
|
||||||
|
|
||||||
|
auto transaction = tokens.begin_transaction();
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
if (!tokens.has_next_token() && allow_blank_layer_name == AllowBlankLayerName::Yes) {
|
||||||
|
// No name present, just return a blank one
|
||||||
|
return FlyString();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& first_name_token = tokens.consume_a_token();
|
||||||
|
if (!is_valid_layer_name_part(first_name_token))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
StringBuilder builder;
|
||||||
|
builder.append(first_name_token.token().ident());
|
||||||
|
|
||||||
|
while (tokens.has_next_token()) {
|
||||||
|
// Repeatedly parse `'.' <ident>`
|
||||||
|
if (!tokens.next_token().is_delim('.'))
|
||||||
|
break;
|
||||||
|
tokens.discard_a_token(); // '.'
|
||||||
|
|
||||||
|
auto& name_token = tokens.consume_a_token();
|
||||||
|
if (!is_valid_layer_name_part(name_token))
|
||||||
|
return {};
|
||||||
|
builder.appendff(".{}", name_token.token().ident());
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit();
|
||||||
|
return builder.to_fly_string_without_validation();
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSRule> Parser::convert_to_layer_rule(AtRule const& rule, Nested nested)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-cascade-5/#at-layer
|
||||||
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
||||||
|
// CSSLayerBlockRule
|
||||||
|
// @layer <layer-name>? {
|
||||||
|
// <rule-list>
|
||||||
|
// }
|
||||||
|
|
||||||
|
// First, the name
|
||||||
|
FlyString layer_name = {};
|
||||||
|
auto prelude_tokens = TokenStream { rule.prelude };
|
||||||
|
if (auto maybe_name = parse_layer_name(prelude_tokens, AllowBlankLayerName::Yes); maybe_name.has_value()) {
|
||||||
|
layer_name = maybe_name.release_value();
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (not a valid layer name) prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
prelude_tokens.discard_whitespace();
|
||||||
|
if (prelude_tokens.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (tokens after layer name) prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then the rules
|
||||||
|
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
||||||
|
rule.for_each_as_rule_list([&](auto& rule) {
|
||||||
|
if (auto child_rule = convert_to_rule(rule, nested))
|
||||||
|
child_rules.append(child_rule);
|
||||||
|
});
|
||||||
|
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
|
||||||
|
return CSSLayerBlockRule::create(m_context.realm(), layer_name, rule_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSSLayerStatementRule
|
||||||
|
// @layer <layer-name>#;
|
||||||
|
auto tokens = TokenStream { rule.prelude };
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
Vector<FlyString> layer_names;
|
||||||
|
while (tokens.has_next_token()) {
|
||||||
|
// Comma
|
||||||
|
if (!layer_names.is_empty()) {
|
||||||
|
if (auto comma = tokens.consume_a_token(); !comma.is(Token::Type::Comma)) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer missing separating comma, ({}) prelude = {}; discarding.", comma.to_debug_string(), rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto name = parse_layer_name(tokens, AllowBlankLayerName::No); name.has_value()) {
|
||||||
|
layer_names.append(name.release_value());
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer contains invalid name, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer_names.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer statement has no layer names, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return CSSLayerStatementRule::create(m_context.realm(), move(layer_names));
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSKeyframesRule> Parser::convert_to_keyframes_rule(AtRule const& rule)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-animations/#keyframes
|
||||||
|
// @keyframes = @keyframes <keyframes-name> { <qualified-rule-list> }
|
||||||
|
// <keyframes-name> = <custom-ident> | <string>
|
||||||
|
// <keyframe-block> = <keyframe-selector># { <declaration-list> }
|
||||||
|
// <keyframe-selector> = from | to | <percentage [0,100]>
|
||||||
|
|
||||||
|
if (rule.prelude.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @keyframes rule: Empty prelude.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Is there some way of detecting if there is a block or not?
|
||||||
|
|
||||||
|
auto prelude_stream = TokenStream { rule.prelude };
|
||||||
|
prelude_stream.discard_whitespace();
|
||||||
|
auto& token = prelude_stream.consume_a_token();
|
||||||
|
if (!token.is_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name_token = token.token();
|
||||||
|
prelude_stream.discard_whitespace();
|
||||||
|
|
||||||
|
if (prelude_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name_token.is(Token::Type::Ident) && (is_css_wide_keyword(name_token.ident()) || name_token.ident().equals_ignoring_ascii_case("none"sv))) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.ident());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.to_debug_string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name = name_token.to_string();
|
||||||
|
|
||||||
|
JS::MarkedVector<CSSRule*> keyframes(m_context.realm().heap());
|
||||||
|
rule.for_each_as_qualified_rule_list([&](auto& qualified_rule) {
|
||||||
|
if (!qualified_rule.child_rules.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes keyframe rule contains at-rules; discarding them.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto selectors = Vector<CSS::Percentage> {};
|
||||||
|
TokenStream child_tokens { qualified_rule.prelude };
|
||||||
|
while (child_tokens.has_next_token()) {
|
||||||
|
child_tokens.discard_whitespace();
|
||||||
|
if (!child_tokens.has_next_token())
|
||||||
|
break;
|
||||||
|
auto tok = child_tokens.consume_a_token();
|
||||||
|
if (!tok.is_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid selector: {}; discarding.", tok.to_debug_string());
|
||||||
|
child_tokens.reconsume_current_input_token();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto token = tok.token();
|
||||||
|
auto read_a_selector = false;
|
||||||
|
if (token.is(Token::Type::Ident)) {
|
||||||
|
if (token.ident().equals_ignoring_ascii_case("from"sv)) {
|
||||||
|
selectors.append(CSS::Percentage(0));
|
||||||
|
read_a_selector = true;
|
||||||
|
}
|
||||||
|
if (token.ident().equals_ignoring_ascii_case("to"sv)) {
|
||||||
|
selectors.append(CSS::Percentage(100));
|
||||||
|
read_a_selector = true;
|
||||||
|
}
|
||||||
|
} else if (token.is(Token::Type::Percentage)) {
|
||||||
|
selectors.append(CSS::Percentage(token.percentage()));
|
||||||
|
read_a_selector = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read_a_selector) {
|
||||||
|
child_tokens.discard_whitespace();
|
||||||
|
if (child_tokens.consume_a_token().is(Token::Type::Comma))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
child_tokens.reconsume_current_input_token();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertiesAndCustomProperties properties;
|
||||||
|
qualified_rule.for_each_as_declaration_list([&](auto const& declaration) {
|
||||||
|
extract_property(declaration, properties);
|
||||||
|
});
|
||||||
|
auto style = PropertyOwningCSSStyleDeclaration::create(m_context.realm(), move(properties.properties), move(properties.custom_properties));
|
||||||
|
for (auto& selector : selectors) {
|
||||||
|
auto keyframe_rule = CSSKeyframeRule::create(m_context.realm(), selector, *style);
|
||||||
|
keyframes.append(keyframe_rule);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return CSSKeyframesRule::create(m_context.realm(), name, CSSRuleList::create(m_context.realm(), move(keyframes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSNamespaceRule> Parser::convert_to_namespace_rule(AtRule const& rule)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-namespaces/#syntax
|
||||||
|
// @namespace <namespace-prefix>? [ <string> | <url> ] ;
|
||||||
|
// <namespace-prefix> = <ident>
|
||||||
|
|
||||||
|
if (rule.prelude.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Empty prelude.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Block is not allowed.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto tokens = TokenStream { rule.prelude };
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
|
||||||
|
Optional<FlyString> prefix = {};
|
||||||
|
if (tokens.next_token().is(Token::Type::Ident)) {
|
||||||
|
prefix = tokens.consume_a_token().token().ident();
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
FlyString namespace_uri;
|
||||||
|
if (auto url = parse_url_function(tokens); url.has_value()) {
|
||||||
|
namespace_uri = MUST(url.value().to_string());
|
||||||
|
} else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) {
|
||||||
|
namespace_uri = url_token.token().string();
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
if (tokens.has_next_token()) {
|
||||||
|
if constexpr (CSS_PARSER_DEBUG) {
|
||||||
|
dbgln("Failed to parse @namespace rule: Trailing tokens after URL.");
|
||||||
|
tokens.dump_all_tokens();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return CSSNamespaceRule::create(m_context.realm(), prefix, namespace_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSSupportsRule> Parser::convert_to_supports_rule(AtRule const& rule, Nested nested)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-conditional-3/#at-supports
|
||||||
|
// @supports <supports-condition> {
|
||||||
|
// <rule-list>
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (rule.prelude.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @supports rule: Empty prelude.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto supports_tokens = TokenStream { rule.prelude };
|
||||||
|
auto supports = parse_a_supports(supports_tokens);
|
||||||
|
if (!supports) {
|
||||||
|
if constexpr (CSS_PARSER_DEBUG) {
|
||||||
|
dbgln("Failed to parse @supports rule: supports clause invalid.");
|
||||||
|
supports_tokens.dump_all_tokens();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
|
||||||
|
rule.for_each_as_rule_list([&](auto& rule) {
|
||||||
|
if (auto child_rule = convert_to_rule(rule, nested))
|
||||||
|
child_rules.append(child_rule);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
|
||||||
|
return CSSSupportsRule::create(m_context.realm(), supports.release_nonnull(), rule_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSPropertyRule> Parser::convert_to_property_rule(AtRule const& rule)
|
||||||
|
{
|
||||||
|
// https://drafts.css-houdini.org/css-properties-values-api-1/#at-ruledef-property
|
||||||
|
// @property <custom-property-name> {
|
||||||
|
// <declaration-list>
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (rule.prelude.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @property rule: Empty prelude.");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto prelude_stream = TokenStream { rule.prelude };
|
||||||
|
prelude_stream.discard_whitespace();
|
||||||
|
auto const& token = prelude_stream.consume_a_token();
|
||||||
|
if (!token.is_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name_token = token.token();
|
||||||
|
prelude_stream.discard_whitespace();
|
||||||
|
|
||||||
|
if (prelude_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name_token.is(Token::Type::Ident)) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name is invalid: {}; discarding.", name_token.to_debug_string());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_a_custom_property_name_string(name_token.ident())) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name doesn't start with '--': {}; discarding.", name_token.ident());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const& name = name_token.ident();
|
||||||
|
|
||||||
|
Optional<FlyString> syntax_maybe;
|
||||||
|
Optional<bool> inherits_maybe;
|
||||||
|
Optional<String> initial_value_maybe;
|
||||||
|
|
||||||
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("syntax"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
|
||||||
|
auto const& syntax_token = token_stream.consume_a_token();
|
||||||
|
if (syntax_token.is(Token::Type::String)) {
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
if (token_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in syntax");
|
||||||
|
} else {
|
||||||
|
syntax_maybe = syntax_token.token().string();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected value for @property \"syntax\": {}; discarding.", declaration.to_string());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("inherits"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
|
||||||
|
auto const& inherits_token = token_stream.consume_a_token();
|
||||||
|
if (inherits_token.is_ident("true"sv) || inherits_token.is_ident("false"sv)) {
|
||||||
|
auto const& ident = inherits_token.token().ident();
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
if (token_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in inherits");
|
||||||
|
} else {
|
||||||
|
inherits_maybe = (ident == "true");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expected true/false for @property \"inherits\" value, got: {}; discarding.", inherits_token.to_debug_string());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("initial-value"sv)) {
|
||||||
|
// FIXME: Ensure that the initial value matches the syntax, and parse the correct CSSValue out
|
||||||
|
StringBuilder initial_value_sb;
|
||||||
|
for (auto const& component : declaration.value) {
|
||||||
|
initial_value_sb.append(component.to_string());
|
||||||
|
}
|
||||||
|
initial_value_maybe = MUST(initial_value_sb.to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (syntax_maybe.has_value() && inherits_maybe.has_value()) {
|
||||||
|
return CSSPropertyRule::create(m_context.realm(), name, syntax_maybe.value(), inherits_maybe.value(), std::move(initial_value_maybe));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
JS::GCPtr<CSSFontFaceRule> Parser::convert_to_font_face_rule(AtRule const& rule)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-fonts/#font-face-rule
|
||||||
|
|
||||||
|
Optional<FlyString> font_family;
|
||||||
|
Optional<FlyString> font_named_instance;
|
||||||
|
Vector<ParsedFontFace::Source> src;
|
||||||
|
Vector<Gfx::UnicodeRange> unicode_range;
|
||||||
|
Optional<int> weight;
|
||||||
|
Optional<int> slope;
|
||||||
|
Optional<int> width;
|
||||||
|
Optional<Percentage> ascent_override;
|
||||||
|
Optional<Percentage> descent_override;
|
||||||
|
Optional<Percentage> line_gap_override;
|
||||||
|
FontDisplay font_display = FontDisplay::Auto;
|
||||||
|
Optional<FlyString> language_override;
|
||||||
|
Optional<OrderedHashMap<FlyString, i64>> font_feature_settings;
|
||||||
|
Optional<OrderedHashMap<FlyString, double>> font_variation_settings;
|
||||||
|
|
||||||
|
// "normal" is returned as nullptr
|
||||||
|
auto parse_as_percentage_or_normal = [&](Vector<ComponentValue> const& values) -> ErrorOr<Optional<Percentage>> {
|
||||||
|
// normal | <percentage [0,∞]>
|
||||||
|
TokenStream tokens { values };
|
||||||
|
if (auto percentage_value = parse_percentage_value(tokens)) {
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
if (tokens.has_next_token())
|
||||||
|
return Error::from_string_literal("Unexpected trailing tokens");
|
||||||
|
|
||||||
|
if (percentage_value->is_percentage() && percentage_value->as_percentage().percentage().value() >= 0)
|
||||||
|
return percentage_value->as_percentage().percentage();
|
||||||
|
|
||||||
|
// TODO: Once we implement calc-simplification in the parser, we should no longer see math values here,
|
||||||
|
// unless they're impossible to resolve and thus invalid.
|
||||||
|
if (percentage_value->is_math()) {
|
||||||
|
if (auto result = percentage_value->as_math().resolve_percentage(); result.has_value())
|
||||||
|
return result.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error::from_string_literal("Invalid percentage");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
if (!tokens.consume_a_token().is_ident("normal"sv))
|
||||||
|
return Error::from_string_literal("Expected `normal | <percentage [0,∞]>`");
|
||||||
|
tokens.discard_whitespace();
|
||||||
|
if (tokens.has_next_token())
|
||||||
|
return Error::from_string_literal("Unexpected trailing tokens");
|
||||||
|
|
||||||
|
return OptionalNone {};
|
||||||
|
};
|
||||||
|
|
||||||
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("ascent-override"sv)) {
|
||||||
|
auto value = parse_as_percentage_or_normal(declaration.value);
|
||||||
|
if (value.is_error()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face ascent-override: {}", value.error());
|
||||||
|
} else {
|
||||||
|
ascent_override = value.release_value();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("descent-override"sv)) {
|
||||||
|
auto value = parse_as_percentage_or_normal(declaration.value);
|
||||||
|
if (value.is_error()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face descent-override: {}", value.error());
|
||||||
|
} else {
|
||||||
|
descent_override = value.release_value();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-display"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto keyword_value = parse_keyword_value(token_stream)) {
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
if (token_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-display");
|
||||||
|
} else {
|
||||||
|
auto value = keyword_to_font_display(keyword_value->to_keyword());
|
||||||
|
if (value.has_value()) {
|
||||||
|
font_display = *value;
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: `{}` is not a valid value for font-display", keyword_value->to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-family"sv)) {
|
||||||
|
// FIXME: This is very similar to, but different from, the logic in parse_font_family_value().
|
||||||
|
// Ideally they could share code.
|
||||||
|
Vector<FlyString> font_family_parts;
|
||||||
|
bool had_syntax_error = false;
|
||||||
|
for (size_t i = 0; i < declaration.value.size(); ++i) {
|
||||||
|
auto const& part = declaration.value[i];
|
||||||
|
if (part.is(Token::Type::Whitespace))
|
||||||
|
continue;
|
||||||
|
if (part.is(Token::Type::String)) {
|
||||||
|
if (!font_family_parts.is_empty()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
||||||
|
had_syntax_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
font_family_parts.append(part.token().string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (part.is(Token::Type::Ident)) {
|
||||||
|
if (is_css_wide_keyword(part.token().ident())) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
||||||
|
had_syntax_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto keyword = keyword_from_string(part.token().ident());
|
||||||
|
if (keyword.has_value() && is_generic_font_family(keyword.value())) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
||||||
|
had_syntax_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
font_family_parts.append(part.token().ident());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
|
||||||
|
had_syntax_error = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (had_syntax_error || font_family_parts.is_empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
font_family = String::join(' ', font_family_parts).release_value_but_fixme_should_propagate_errors();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-feature-settings"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto value = parse_css_value(CSS::PropertyID::FontFeatureSettings, token_stream); !value.is_error()) {
|
||||||
|
if (value.value()->to_keyword() == Keyword::Normal) {
|
||||||
|
font_feature_settings.clear();
|
||||||
|
} else if (value.value()->is_value_list()) {
|
||||||
|
auto const& feature_tags = value.value()->as_value_list().values();
|
||||||
|
OrderedHashMap<FlyString, i64> settings;
|
||||||
|
settings.ensure_capacity(feature_tags.size());
|
||||||
|
for (auto const& feature_tag : feature_tags) {
|
||||||
|
if (!feature_tag->is_open_type_tagged()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const& setting_value = feature_tag->as_open_type_tagged().value();
|
||||||
|
if (setting_value->is_integer()) {
|
||||||
|
settings.set(feature_tag->as_open_type_tagged().tag(), setting_value->as_integer().integer());
|
||||||
|
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
|
||||||
|
if (auto integer = setting_value->as_math().resolve_integer(); integer.has_value()) {
|
||||||
|
settings.set(feature_tag->as_open_type_tagged().tag(), *integer);
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-feature-settings descriptor cannot be resolved at parse time; skipping");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue holding a <integer>; skipping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font_feature_settings = move(settings);
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-feature-settings descriptor, not compatible with value returned from parsing font-feature-settings property: {}", value.value()->to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-language-override"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto maybe_value = parse_css_value(CSS::PropertyID::FontLanguageOverride, token_stream); !maybe_value.is_error()) {
|
||||||
|
auto& value = maybe_value.value();
|
||||||
|
if (value->is_string()) {
|
||||||
|
language_override = value->as_string().string_value();
|
||||||
|
} else {
|
||||||
|
language_override.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-named-instance"sv)) {
|
||||||
|
// auto | <string>
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
auto& token = token_stream.consume_a_token();
|
||||||
|
token_stream.discard_whitespace();
|
||||||
|
if (token_stream.has_next_token()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-named-instance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.is_ident("auto"sv)) {
|
||||||
|
font_named_instance.clear();
|
||||||
|
} else if (token.is(Token::Type::String)) {
|
||||||
|
font_named_instance = token.token().string();
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-named-instance from {}", token.to_debug_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-style"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto value = parse_css_value(CSS::PropertyID::FontStyle, token_stream); !value.is_error()) {
|
||||||
|
slope = value.value()->to_font_slope();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-variation-settings"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto value = parse_css_value(CSS::PropertyID::FontVariationSettings, token_stream); !value.is_error()) {
|
||||||
|
if (value.value()->to_keyword() == Keyword::Normal) {
|
||||||
|
font_variation_settings.clear();
|
||||||
|
} else if (value.value()->is_value_list()) {
|
||||||
|
auto const& variation_tags = value.value()->as_value_list().values();
|
||||||
|
OrderedHashMap<FlyString, double> settings;
|
||||||
|
settings.ensure_capacity(variation_tags.size());
|
||||||
|
for (auto const& variation_tag : variation_tags) {
|
||||||
|
if (!variation_tag->is_open_type_tagged()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const& setting_value = variation_tag->as_open_type_tagged().value();
|
||||||
|
if (setting_value->is_number()) {
|
||||||
|
settings.set(variation_tag->as_open_type_tagged().tag(), setting_value->as_number().number());
|
||||||
|
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
|
||||||
|
if (auto number = setting_value->as_math().resolve_number(); number.has_value()) {
|
||||||
|
settings.set(variation_tag->as_open_type_tagged().tag(), *number);
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-variation-settings descriptor cannot be resolved at parse time; skipping");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue holding a <number>; skipping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font_variation_settings = move(settings);
|
||||||
|
} else {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-variation-settings descriptor, not compatible with value returned from parsing font-variation-settings property: {}", value.value()->to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-weight"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto value = parse_css_value(CSS::PropertyID::FontWeight, token_stream); !value.is_error()) {
|
||||||
|
weight = value.value()->to_font_weight();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("font-width"sv)
|
||||||
|
|| declaration.name.equals_ignoring_ascii_case("font-stretch"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
if (auto value = parse_css_value(CSS::PropertyID::FontWidth, token_stream); !value.is_error()) {
|
||||||
|
width = value.value()->to_font_width();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("line-gap-override"sv)) {
|
||||||
|
auto value = parse_as_percentage_or_normal(declaration.value);
|
||||||
|
if (value.is_error()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face line-gap-override: {}", value.error());
|
||||||
|
} else {
|
||||||
|
line_gap_override = value.release_value();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("src"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
Vector<ParsedFontFace::Source> supported_sources = parse_font_face_src(token_stream);
|
||||||
|
if (!supported_sources.is_empty())
|
||||||
|
src = move(supported_sources);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaration.name.equals_ignoring_ascii_case("unicode-range"sv)) {
|
||||||
|
TokenStream token_stream { declaration.value };
|
||||||
|
auto unicode_ranges = parse_unicode_ranges(token_stream);
|
||||||
|
if (unicode_ranges.is_empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
unicode_range = move(unicode_ranges);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unrecognized descriptor '{}' in @font-face; discarding.", declaration.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!font_family.has_value()) {
|
||||||
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face: no font-family!");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unicode_range.is_empty()) {
|
||||||
|
unicode_range.empend(0x0u, 0x10FFFFu);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CSSFontFaceRule::create(m_context.realm(), ParsedFontFace { font_family.release_value(), move(weight), move(slope), move(width), move(src), move(unicode_range), move(ascent_override), move(descent_override), move(line_gap_override), font_display, move(font_named_instance), move(language_override), move(font_feature_settings), move(font_variation_settings) });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue