2024-10-31 16:03:06 +00:00
/*
* 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>
2025-01-27 21:09:12 +01:00
# include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
2024-10-31 16:03:06 +00:00
# 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 {
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSRule > Parser : : convert_to_rule ( Rule const & rule , Nested nested )
2024-10-31 16:03:06 +00:00
{
return rule . visit (
2024-11-15 04:01:23 +13:00
[ this , nested ] ( AtRule const & at_rule ) - > GC : : Ptr < CSSRule > {
2024-10-31 16:03:06 +00:00
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 { } ;
} ,
2024-11-15 04:01:23 +13:00
[ this , nested ] ( QualifiedRule const & qualified_rule ) - > GC : : Ptr < CSSRule > {
2024-10-31 16:03:06 +00:00
return convert_to_style_rule ( qualified_rule , nested ) ;
} ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSStyleRule > Parser : : convert_to_style_rule ( QualifiedRule const & qualified_rule , Nested nested )
2024-10-31 16:03:06 +00:00
{
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 ( ) ;
2024-11-08 17:50:38 +00:00
if ( nested = = Nested : : Yes )
selectors = adapt_nested_relative_selector_list ( selectors ) ;
2024-10-31 16:03:06 +00:00
auto * declaration = convert_to_style_declaration ( qualified_rule . declarations ) ;
if ( ! declaration ) {
dbgln_if ( CSS_PARSER_DEBUG , " CSSParser: style rule declaration invalid; discarding. " ) ;
return { } ;
}
2024-12-26 14:32:52 +01:00
GC : : RootVector < CSSRule * > child_rules { m_context . realm ( ) . heap ( ) } ;
2024-10-31 16:03:06 +00:00
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 ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSImportRule > Parser : : convert_to_import_rule ( AtRule const & rule )
2024-10-31 16:03:06 +00:00
{
// 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 ( ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSRule > Parser : : convert_to_layer_rule ( AtRule const & rule , Nested nested )
2024-10-31 16:03:06 +00:00
{
// 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
2024-12-26 14:32:52 +01:00
GC : : RootVector < CSSRule * > child_rules { m_context . realm ( ) . heap ( ) } ;
2024-11-04 17:26:19 +00:00
for ( auto const & child : rule . child_rules_and_lists_of_declarations ) {
child . visit (
[ & ] ( Rule const & rule ) {
if ( auto child_rule = convert_to_rule ( rule , nested ) )
child_rules . append ( child_rule ) ;
} ,
[ & ] ( 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 ) ) ;
} ) ;
}
2024-10-31 16:03:06 +00:00
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 ) ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSKeyframesRule > Parser : : convert_to_keyframes_rule ( AtRule const & rule )
2024-10-31 16:03:06 +00:00
{
// 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 ( ) ;
2024-12-26 14:32:52 +01:00
GC : : RootVector < CSSRule * > keyframes ( m_context . realm ( ) . heap ( ) ) ;
2024-10-31 16:03:06 +00:00
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 ) ) ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSNamespaceRule > Parser : : convert_to_namespace_rule ( AtRule const & rule )
2024-10-31 16:03:06 +00:00
{
// 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 ( ) ) {
2024-12-03 22:31:33 +13:00
namespace_uri = url . value ( ) . to_string ( ) ;
2024-10-31 16:03:06 +00:00
} 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 ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSSupportsRule > Parser : : convert_to_supports_rule ( AtRule const & rule , Nested nested )
2024-10-31 16:03:06 +00:00
{
// 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 { } ;
}
2024-12-26 14:32:52 +01:00
GC : : RootVector < CSSRule * > child_rules { m_context . realm ( ) . heap ( ) } ;
2024-11-04 17:26:19 +00:00
for ( auto const & child : rule . child_rules_and_lists_of_declarations ) {
child . visit (
[ & ] ( Rule const & rule ) {
if ( auto child_rule = convert_to_rule ( rule , nested ) )
child_rules . append ( child_rule ) ;
} ,
[ & ] ( 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 ) ) ;
} ) ;
}
2024-10-31 16:03:06 +00:00
auto rule_list = CSSRuleList : : create ( m_context . realm ( ) , child_rules ) ;
return CSSSupportsRule : : create ( m_context . realm ( ) , supports . release_nonnull ( ) , rule_list ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSPropertyRule > Parser : : convert_to_property_rule ( AtRule const & rule )
2024-10-31 16:03:06 +00:00
{
// 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 { } ;
}
2024-11-15 04:01:23 +13:00
GC : : Ptr < CSSFontFaceRule > Parser : : convert_to_font_face_rule ( AtRule const & rule )
2024-10-31 16:03:06 +00:00
{
// 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.
2024-12-11 15:05:56 +00:00
if ( percentage_value - > is_calculated ( ) ) {
LibWeb/CSS: Wrap calc()-resolution data in a struct
Initially I added this to the existing CalculationContext, but in
reality, we have some data at parse-time and different data at
resolve-time, so it made more sense to keep those separate.
Instead of needing a variety of methods for resolving a Foo, depending
on whether we have a Layout::Node available, or a percentage basis, or
a length resolution context... put those in a
CalculationResolutionContext, and just pass that one thing to these
methods. This also removes the need for separate resolve_*_percentage()
methods, because we can just pass the percentage basis in to the regular
resolve_foo() method.
This also corrects the issue that *any* calculation may need to resolve
lengths, but we previously only passed a length resolution context to
specific types in some situations. Now, they can all have one available,
though it's up to the caller to provide it.
2025-01-22 16:05:32 +00:00
if ( auto result = percentage_value - > as_calculated ( ) . resolve_percentage ( { } ) ; result . has_value ( ) )
2024-10-31 16:03:06 +00:00
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 {
2024-12-13 13:51:45 +00:00
dbgln_if ( CSS_PARSER_DEBUG , " CSSParser: `{}` is not a valid value for font-display " , keyword_value - > to_string ( CSSStyleValue : : SerializationMode : : Normal ) ) ;
2024-10-31 16:03:06 +00:00
}
}
}
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 ( ) ) ;
2024-12-11 15:05:56 +00:00
} else if ( setting_value - > is_calculated ( ) & & setting_value - > as_calculated ( ) . resolves_to_number ( ) ) {
LibWeb/CSS: Wrap calc()-resolution data in a struct
Initially I added this to the existing CalculationContext, but in
reality, we have some data at parse-time and different data at
resolve-time, so it made more sense to keep those separate.
Instead of needing a variety of methods for resolving a Foo, depending
on whether we have a Layout::Node available, or a percentage basis, or
a length resolution context... put those in a
CalculationResolutionContext, and just pass that one thing to these
methods. This also removes the need for separate resolve_*_percentage()
methods, because we can just pass the percentage basis in to the regular
resolve_foo() method.
This also corrects the issue that *any* calculation may need to resolve
lengths, but we previously only passed a length resolution context to
specific types in some situations. Now, they can all have one available,
though it's up to the caller to provide it.
2025-01-22 16:05:32 +00:00
if ( auto integer = setting_value - > as_calculated ( ) . resolve_integer ( { } ) ; integer . has_value ( ) ) {
2024-10-31 16:03:06 +00:00
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 {
2024-12-13 13:51:45 +00:00
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 ( CSSStyleValue : : SerializationMode : : Normal ) ) ;
2024-10-31 16:03:06 +00:00
}
}
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 ( ) ) ;
2024-12-11 15:05:56 +00:00
} else if ( setting_value - > is_calculated ( ) & & setting_value - > as_calculated ( ) . resolves_to_number ( ) ) {
LibWeb/CSS: Wrap calc()-resolution data in a struct
Initially I added this to the existing CalculationContext, but in
reality, we have some data at parse-time and different data at
resolve-time, so it made more sense to keep those separate.
Instead of needing a variety of methods for resolving a Foo, depending
on whether we have a Layout::Node available, or a percentage basis, or
a length resolution context... put those in a
CalculationResolutionContext, and just pass that one thing to these
methods. This also removes the need for separate resolve_*_percentage()
methods, because we can just pass the percentage basis in to the regular
resolve_foo() method.
This also corrects the issue that *any* calculation may need to resolve
lengths, but we previously only passed a length resolution context to
specific types in some situations. Now, they can all have one available,
though it's up to the caller to provide it.
2025-01-22 16:05:32 +00:00
if ( auto number = setting_value - > as_calculated ( ) . resolve_number ( { } ) ; number . has_value ( ) ) {
2024-10-31 16:03:06 +00:00
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 {
2024-12-13 13:51:45 +00:00
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 ( CSSStyleValue : : SerializationMode : : Normal ) ) ;
2024-10-31 16:03:06 +00:00
}
}
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 ) } ) ;
}
}