diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b5448370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,484 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_class_decorator_wrap = split_into_lines +ij_typescript_class_field_decorator_wrap = off +ij_typescript_class_method_decorator_wrap = off +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_function_parameter_decorator_wrap = off +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.es6,*.js,*.mjs}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_class_decorator_wrap = split_into_lines +ij_javascript_class_field_decorator_wrap = off +ij_javascript_class_method_decorator_wrap = off +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_function_parameter_decorator_wrap = off +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitattributes b/.gitattributes index a7167808..0c62b810 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ /test/ export-ignore /docs/ export-ignore +/.editorconfig export-ignore /build.sh export-ignore linguist-vendored /Writerside export-ignore linguist-vendored /benchmark diff --git a/CHANGELOG.md b/CHANGELOG.md index cf049876..79476253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +# v0.9.0 + +media query level 5 +- [x] at-rule custom-media +- [x] at-rule when-else custom media +- [x] at-rule charset validation +- [x] media query error handling +- [x] at-rule container +- [ ] expand at-rule custom-media +- [ ] expand at-rule when-else + +selector validation +- [ ] pseudo class arguments validation +- [ ] pseudo class validation + +declaration validation +- [ ] validate declaration + +error validation +- [ ] when a parent is marked as invalid node, do not parse or validate descendant nodes + # v0.8.0 - [x] validate selectors using mdn data diff --git a/README.md b/README.md index bd289c86..10b9b911 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![playground](https://img.shields.io/badge/playground-try%20it%20now-%230a7398 -)](https://tbela99.github.io/css-parser/playground/)[![npm](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftbela99%2Fcss-parser%2Fmaster%2Fpackage.json&query=version&logo=npm&label=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40tbela99%2Fcss-parser)](https://www.npmjs.com/package/@tbela99/css-parser) [![npm](https://img.shields.io/jsr/v/%40tbela99/css-parser?link=https%3A%2F%2Fjsr.io%2F%40tbela99%2Fcss-parser +)](https://tbela99.github.io/css-parser/playground/) [![npm](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftbela99%2Fcss-parser%2Fmaster%2Fpackage.json&query=version&logo=npm&label=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40tbela99%2Fcss-parser)](https://www.npmjs.com/package/@tbela99/css-parser) [![npm](https://img.shields.io/jsr/v/%40tbela99/css-parser?link=https%3A%2F%2Fjsr.io%2F%40tbela99%2Fcss-parser )](https://jsr.io/@tbela99/css-parser) [![cov](https://tbela99.github.io/css-parser/badges/coverage.svg)](https://github.com/tbela99/css-parser/actions) [![NPM Downloads](https://img.shields.io/npm/dm/%40tbela99%2Fcss-parser)](https://www.npmjs.com/package/@tbela99/css-parser) # css-parser @@ -187,7 +187,8 @@ Include ParseOptions and RenderOptions > Minify Options -- minify: boolean, optional. default to _true_. minify css output. +- beautify: boolean, optional. default to _false_. beautify css output. +- minify: boolean, optional. default to _true_. minify css values. - withParents: boolean, optional. render this node and its parents. - removeEmpty: boolean, optional. remove empty rule lists from the ast. - expandNestingRules: boolean, optional. expand nesting rules. diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index ee5e2f99..43048aff 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -3333,16 +3333,21 @@ } } function doRender(data, options = {}) { + const minify = options.minify ?? true; + const beautify = options.beautify ?? !minify; options = { - ...(options.minify ?? true ? { + ...(beautify ? { + indent: ' ', + newLine: '\n', + } : { indent: '', newLine: '', + }), + ...(minify ? { removeEmpty: true, removeComments: true } : { - indent: ' ', - newLine: '\n', - compress: false, + removeEmpty: false, removeComments: false, }), sourcemap: false, convertColor: true, expandNestingRules: false, preserveLicense: false, ...options }; @@ -3934,6 +3939,341 @@ 'aural', 'braille', 'embossed', 'handheld', 'projection', 'tty', 'tv', 'speech']; // https://www.w3.org/TR/css-values-4/#math-function const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; + const webkitPseudoAliasMap = { + '-webkit-autofill': 'autofill', + '-webkit-any': 'is', + '-moz-any': 'is', + '-webkit-border-after': 'border-block-end', + '-webkit-border-after-color': 'border-block-end-color', + '-webkit-border-after-style': 'border-block-end-style', + '-webkit-border-after-width': 'border-block-end-width', + '-webkit-border-before': 'border-block-start', + '-webkit-border-before-color': 'border-block-start-color', + '-webkit-border-before-style': 'border-block-start-style', + '-webkit-border-before-width': 'border-block-start-width', + '-webkit-border-end': 'border-inline-end', + '-webkit-border-end-color': 'border-inline-end-color', + '-webkit-border-end-style': 'border-inline-end-style', + '-webkit-border-end-width': 'border-inline-end-width', + '-webkit-border-start': 'border-inline-start', + '-webkit-border-start-color': 'border-inline-start-color', + '-webkit-border-start-style': 'border-inline-start-style', + '-webkit-border-start-width': 'border-inline-start-width', + '-webkit-box-align': 'align-items', + '-webkit-box-direction': 'flex-direction', + '-webkit-box-flex': 'flex-grow', + '-webkit-box-lines': 'flex-flow', + '-webkit-box-ordinal-group': 'order', + '-webkit-box-orient': 'flex-direction', + '-webkit-box-pack': 'justify-content', + '-webkit-column-break-after': 'break-after', + '-webkit-column-break-before': 'break-before', + '-webkit-column-break-inside': 'break-inside', + '-webkit-font-feature-settings': 'font-feature-settings', + '-webkit-hyphenate-character': 'hyphenate-character', + '-webkit-initial-letter': 'initial-letter', + '-webkit-margin-end': 'margin-block-end', + '-webkit-margin-start': 'margin-block-start', + '-webkit-padding-after': 'padding-block-end', + '-webkit-padding-before': 'padding-block-start', + '-webkit-padding-end': 'padding-inline-end', + '-webkit-padding-start': 'padding-inline-start', + '-webkit-min-device-pixel-ratio': 'min-resolution', + '-webkit-max-device-pixel-ratio': 'max-resolution' + }; + // https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions + // https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar + const webkitExtensions = new Set([ + '-webkit-app-region', + '-webkit-border-horizontal-spacing', + '-webkit-border-vertical-spacing', + '-webkit-box-reflect', + '-webkit-column-axis', + '-webkit-column-progression', + '-webkit-cursor-visibility', + '-webkit-font-smoothing', + '-webkit-hyphenate-limit-after', + '-webkit-hyphenate-limit-before', + '-webkit-hyphenate-limit-lines', + '-webkit-line-align', + '-webkit-line-box-contain', + '-webkit-line-clamp', + '-webkit-line-grid', + '-webkit-line-snap', + '-webkit-locale', + '-webkit-logical-height', + '-webkit-logical-width', + '-webkit-margin-after', + '-webkit-margin-before', + '-webkit-mask-box-image-outset', + '-webkit-mask-box-image-repeat', + '-webkit-mask-box-image-slice', + '-webkit-mask-box-image-source', + '-webkit-mask-box-image-width', + '-webkit-mask-box-image', + '-webkit-mask-composite', + '-webkit-mask-position-x', + '-webkit-mask-position-y', + '-webkit-mask-repeat-x', + '-webkit-mask-repeat-y', + '-webkit-mask-source-type', + '-webkit-max-logical-height', + '-webkit-max-logical-width', + '-webkit-min-logical-height', + '-webkit-min-logical-width', + '-webkit-nbsp-mode', + '-webkit-perspective-origin-x', + '-webkit-perspective-origin-y', + '-webkit-rtl-ordering', + '-webkit-tap-highlight-color', + '-webkit-text-decoration-skip', + '-webkit-text-decorations-in-effect', + '-webkit-text-fill-color', + '-webkit-text-security', + '-webkit-text-stroke-color', + '-webkit-text-stroke-width', + '-webkit-text-stroke', + '-webkit-text-zoom', + '-webkit-touch-callout', + '-webkit-transform-origin-x', + '-webkit-transform-origin-y', + '-webkit-transform-origin-z', + '-webkit-user-drag', + '-webkit-user-modify', + '-webkit-border-after', + '-webkit-border-after-color', + '-webkit-border-after-style', + '-webkit-border-after-width', + '-webkit-border-before', + '-webkit-border-before-color', + '-webkit-border-before-style', + '-webkit-border-before-width', + '-webkit-border-end', + '-webkit-border-end-color', + '-webkit-border-end-style', + '-webkit-border-end-width', + '-webkit-border-start', + '-webkit-border-start-color', + '-webkit-border-start-style', + '-webkit-border-start-width', + '-webkit-box-align', + '-webkit-box-direction', + '-webkit-box-flex-group', + '-webkit-box-flex', + '-webkit-box-lines', + '-webkit-box-ordinal-group', + '-webkit-box-orient', + '-webkit-box-pack', + '-webkit-column-break-after', + '-webkit-column-break-before', + '-webkit-column-break-inside', + '-webkit-font-feature-settings', + '-webkit-hyphenate-character', + '-webkit-initial-letter', + '-webkit-margin-end', + '-webkit-margin-start', + '-webkit-padding-after', + '-webkit-padding-before', + '-webkit-padding-end', + '-webkit-padding-start', + '-webkit-fill-available', + ':-webkit-animating-full-screen-transition', + ':-webkit-any', + ':-webkit-any-link', + ':-webkit-autofill', + ':-webkit-autofill-strong-password', + ':-webkit-drag', + ':-webkit-full-page-media', + ':-webkit-full-screen*', + ':-webkit-full-screen-ancestor', + ':-webkit-full-screen-document', + ':-webkit-full-screen-controls-hidden', + '::-webkit-file-upload-button*', + '::-webkit-inner-spin-button', + '::-webkit-input-placeholder', + '::-webkit-meter-bar', + '::-webkit-meter-even-less-good-value', + '::-webkit-meter-inner-element', + '::-webkit-meter-optimum-value', + '::-webkit-meter-suboptimum-value', + '::-webkit-progress-bar', + '::-webkit-progress-inner-element', + '::-webkit-progress-value', + '::-webkit-search-cancel-button', + '::-webkit-search-results-button', + '::-webkit-slider-runnable-track', + '::-webkit-slider-thumb', + '-webkit-animation', + '-webkit-device-pixel-ratio', + '-webkit-transform-2d', + '-webkit-transform-3d', + '-webkit-transition', + '::-webkit-scrollbar', + '::-webkit-scrollbar-button', + '::-webkit-scrollbar', + '::-webkit-scrollbar-thumb', + '::-webkit-scrollbar-track', + '::-webkit-scrollbar-track-piece', + '::-webkit-scrollbar:vertical', + '::-webkit-scrollbar-corner ', + '::-webkit-resizer', + ':vertical', + ':horizontal', + ]); + // https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions + const mozExtensions = new Set([ + '-moz-box-align', + '-moz-box-direction', + '-moz-box-flex', + '-moz-box-ordinal-group', + '-moz-box-orient', + '-moz-box-pack', + '-moz-float-edge', + '-moz-force-broken-image-icon', + '-moz-image-region', + '-moz-orient', + '-moz-osx-font-smoothing', + '-moz-user-focus', + '-moz-user-input', + '-moz-user-modify', + '-moz-animation', + '-moz-animation-delay', + '-moz-animation-direction', + '-moz-animation-duration', + '-moz-animation-fill-mode', + '-moz-animation-iteration-count', + '-moz-animation-name', + '-moz-animation-play-state', + '-moz-animation-timing-function', + '-moz-appearance', + '-moz-backface-visibility', + '-moz-background-clip', + '-moz-background-origin', + '-moz-background-inline-policy', + '-moz-background-size', + '-moz-border-end', + '-moz-border-end-color', + '-moz-border-end-style', + '-moz-border-end-width', + '-moz-border-image', + '-moz-border-start', + '-moz-border-start-color', + '-moz-border-start-style', + '-moz-border-start-width', + '-moz-box-sizing', + 'clip-path', + '-moz-column-count', + '-moz-column-fill', + '-moz-column-gap', + '-moz-column-width', + '-moz-column-rule', + '-moz-column-rule-width', + '-moz-column-rule-style', + '-moz-column-rule-color', + 'filter', + '-moz-font-feature-settings', + '-moz-font-language-override', + '-moz-hyphens', + '-moz-margin-end', + '-moz-margin-start', + 'mask', + '-moz-opacity', + '-moz-outline', + '-moz-outline-color', + '-moz-outline-offset', + '-moz-outline-style', + '-moz-outline-width', + '-moz-padding-end', + '-moz-padding-start', + '-moz-perspective', + '-moz-perspective-origin', + 'pointer-events', + '-moz-tab-size', + '-moz-text-align-last', + '-moz-text-decoration-color', + '-moz-text-decoration-line', + '-moz-text-decoration-style', + '-moz-text-size-adjust', + '-moz-transform', + '-moz-transform-origin', + '-moz-transform-style', + '-moz-transition', + '-moz-transition-delay', + '-moz-transition-duration', + '-moz-transition-property', + '-moz-transition-timing-function', + '-moz-user-select', + '-moz-initial', + '-moz-appearance', + '-moz-linear-gradient', + '-moz-radial-gradient', + '-moz-element', + '-moz-image-rect', + '::-moz-anonymous-block', + '::-moz-anonymous-positioned-block', + ':-moz-any', + ':-moz-any-link', + ':-moz-broken', + '::-moz-canvas', + '::-moz-color-swatch', + '::-moz-cell-content', + ':-moz-drag-over', + ':-moz-first-node', + '::-moz-focus-inner', + '::-moz-focus-outer', + ':-moz-full-screen', + ':-moz-full-screen-ancestor', + ':-moz-handler-blocked', + ':-moz-handler-crashed', + ':-moz-handler-disabled', + '::-moz-inline-table', + ':-moz-last-node', + '::-moz-list-bullet', + '::-moz-list-number', + ':-moz-loading', + ':-moz-locale-dir', + ':-moz-locale-dir', + ':-moz-lwtheme', + ':-moz-lwtheme-brighttext', + ':-moz-lwtheme-darktext', + '::-moz-meter-bar', + ':-moz-native-anonymous', + ':-moz-only-whitespace', + '::-moz-pagebreak', + '::-moz-pagecontent', + ':-moz-placeholder', + '::-moz-placeholder', + '::-moz-progress-bar', + '::-moz-range-progress', + '::-moz-range-thumb', + '::-moz-range-track', + ':-moz-read-only', + ':-moz-read-write', + '::-moz-scrolled-canvas', + '::-moz-scrolled-content', + '::-moz-selection', + ':-moz-submit-invalid', + ':-moz-suppressed', + '::-moz-svg-foreign-content', + '::-moz-table', + '::-moz-table-cell', + '::-moz-table-column', + '::-moz-table-column-group', + '::-moz-table-outer', + '::-moz-table-row', + '::-moz-table-row-group', + ':-moz-ui-invalid', + ':-moz-ui-valid', + ':-moz-user-disabled', + '::-moz-viewport', + '::-moz-viewport-scroll', + ':-moz-window-inactive', + '-moz-device-pixel-ratio', + '-moz-os-version', + '-moz-touch-enabled', + '-moz-windows-glass', + '-moz-alt-content' + ]); function isLength(dimension) { return 'unit' in dimension && dimensionUnits.has(dimension.unit.toLowerCase()); } @@ -5954,7 +6294,7 @@ return count; } function pushToken(token, parseInfo, hint) { - const result = { token, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; + const result = { token, len: parseInfo.currentPosition.ind - parseInfo.position.ind, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; parseInfo.position.ind = parseInfo.currentPosition.ind; parseInfo.position.lin = parseInfo.currentPosition.lin; parseInfo.position.col = Math.max(parseInfo.currentPosition.col, 1); @@ -6122,8 +6462,10 @@ buffer += value; } } - yield pushToken(buffer, parseInfo, exports.EnumToken.BadCommentTokenType); - buffer = ''; + if (buffer.length > 0) { + yield pushToken(buffer, parseInfo, exports.EnumToken.BadCommentTokenType); + buffer = ''; + } } break; case '&': @@ -11262,12 +11604,12 @@ return true; } - function splitTokenList(tokenList) { + function splitTokenList(tokenList, split = [exports.EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == exports.EnumToken.CommentTokenType) { return acc; } - if (curr.typ == exports.EnumToken.CommaTokenType) { + if (split.includes(curr.typ)) { acc.push([]); } else { @@ -11358,38 +11700,27 @@ }; } - const combinatorsTokens = [exports.EnumToken.ChildCombinatorTokenType, exports.EnumToken.ColumnCombinatorTokenType, - exports.EnumToken.DescendantCombinatorTokenType, exports.EnumToken.NextSiblingCombinatorTokenType, exports.EnumToken.SubsequentSiblingCombinatorTokenType]; - // [ ? ]* - function validateComplexSelector(tokens, root, options) { - // [ ? * [ * ]* ]! - tokens = tokens.slice(); - consumeWhitespace(tokens); + function validateCompoundSelector(tokens, root, options) { if (tokens.length == 0) { + // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], // @ts-ignore node: root, + // @ts-ignore syntax: null, error: 'expected selector', tokens }; } + tokens = tokens.slice(); + consumeWhitespace(tokens); + const config = getSyntaxConfig(); + let match = 0; + let length = tokens.length; while (tokens.length > 0) { - if (combinatorsTokens.includes(tokens[0].typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'unexpected combinator', - tokens - }; - } - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { if (!options?.nestedSelector) { // @ts-ignore return { @@ -11402,105 +11733,120 @@ tokens }; } - while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - tokens.shift(); - consumeWhitespace(tokens); - } - if (tokens.length == 0) { - break; - } - } - if (exports.EnumToken.IdenTokenType == tokens[0].typ) { + match++; tokens.shift(); consumeWhitespace(tokens); - if (tokens.length == 0) { - break; - } } - if (exports.EnumToken.UniversalSelectorTokenType == tokens[0].typ) { + // + while (tokens.length > 0 && + [ + exports.EnumToken.IdenTokenType, + exports.EnumToken.NameSpaceAttributeTokenType, + exports.EnumToken.ClassSelectorTokenType, + exports.EnumToken.HashTokenType, + exports.EnumToken.UniversalSelectorTokenType + ].includes(tokens[0].typ)) { + match++; tokens.shift(); consumeWhitespace(tokens); } - while (tokens.length > 0) { - if (tokens[0].typ == exports.EnumToken.PseudoClassFuncTokenType) { - if (tokens[0].val.startsWith(':-webkit-')) { + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.PseudoClassFuncTokenType) { + if (!mozExtensions.has(tokens[0].val + '()') && + !webkitExtensions.has(tokens[0].val + '()') && + !((tokens[0].val + '()') in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'invalid pseudo-class', - tokens - }; - } - } - if ([ - exports.EnumToken.ClassSelectorTokenType, - exports.EnumToken.HashTokenType, - exports.EnumToken.PseudoClassTokenType, - exports.EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - tokens.shift(); - consumeWhitespace(tokens); - continue; + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val + '()', + tokens + }; } - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.PseudoClassTokenType) { + const isPseudoElement = tokens[0].val.startsWith('::'); + if ( + // https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions#pseudo-elements + !(isPseudoElement && tokens[0].val.startsWith('::-webkit-')) && + !mozExtensions.has(tokens[0].val) && + !webkitExtensions.has(tokens[0].val) && + !(tokens[0].val in config.selectors) && + !(!isPseudoElement && + (':' + tokens[0].val) in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val, + tokens + }; } - // validate namespace - if (tokens[0].typ == exports.EnumToken.NameSpaceAttributeTokenType) { - if (!((tokens[0].l == null || tokens[0].l.typ == exports.EnumToken.IdenTokenType || (tokens[0].l.typ == exports.EnumToken.LiteralTokenType && tokens[0].l.val == '*')) && - tokens[0].r.typ == exports.EnumToken.IdenTokenType)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting wq-name', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.AttrTokenType) { + const children = tokens[0].chi.slice(); + consumeWhitespace(children); + if (children.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; } - // validate attribute - else if (tokens[0].typ == exports.EnumToken.AttrTokenType) { - const children = tokens[0].chi.slice(); - consumeWhitespace(children); - if (children.length == 0) { - // @ts-ignore + if (![ + exports.EnumToken.IdenTokenType, + exports.EnumToken.NameSpaceAttributeTokenType, + exports.EnumToken.MatchExpressionTokenType + ].includes(children[0].typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + if (children[0].typ == exports.EnumToken.MatchExpressionTokenType) { + if (children.length != 1) { return { valid: ValidationLevel.Drop, matches: [], node: tokens[0], syntax: null, - error: 'invalid attribute selector', + error: 'invalid ', tokens }; } if (![ exports.EnumToken.IdenTokenType, - exports.EnumToken.NameSpaceAttributeTokenType, - exports.EnumToken.MatchExpressionTokenType - ].includes(children[0].typ)) { + exports.EnumToken.NameSpaceAttributeTokenType + ].includes(children[0].l.typ) || + ![ + exports.EnumToken.EqualMatchTokenType, exports.EnumToken.DashMatchTokenType, + exports.EnumToken.StartMatchTokenType, exports.EnumToken.ContainMatchTokenType, + exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType + ].includes(children[0].op.typ) || + ![ + exports.EnumToken.StringTokenType, + exports.EnumToken.IdenTokenType + ].includes(children[0].r.typ)) { // @ts-ignore return { valid: ValidationLevel.Drop, @@ -11511,145 +11857,115 @@ tokens }; } - if (children[0].typ == exports.EnumToken.MatchExpressionTokenType) { - if (![exports.EnumToken.IdenTokenType, - exports.EnumToken.NameSpaceAttributeTokenType].includes(children[0].l.typ) || - ![ - exports.EnumToken.EqualMatchTokenType, exports.EnumToken.DashMatchTokenType, - exports.EnumToken.StartMatchTokenType, exports.EnumToken.ContainMatchTokenType, - exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType - ].includes(children[0].op.typ) || - ![exports.EnumToken.StringTokenType, - exports.EnumToken.IdenTokenType].includes(children[0].r.typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - } - children.shift(); - consumeWhitespace(children); - if (children.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: null, - error: 'unexpected token', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - break; - } - if (tokens.length == 0) { - break; - } - // combinator - if (!combinatorsTokens.includes(tokens[0].typ)) { - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { + if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], - // @ts-ignore node: tokens[0], syntax: null, - error: 'nested selector not allowed', + error: 'invalid attribute selector', tokens }; } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - if (tokens.length > 0 && - [ - exports.EnumToken.IdenTokenType, - exports.EnumToken.AttrTokenType, - exports.EnumToken.NameSpaceAttributeTokenType, - exports.EnumToken.ClassSelectorTokenType, - exports.EnumToken.HashTokenType, - exports.EnumToken.PseudoClassTokenType, - exports.EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - continue; } - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting combinator or subclass-selector', - tokens - }; + match++; + tokens.shift(); + consumeWhitespace(tokens); } - const token = tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - // @ts-ignore + if (length == tokens.length) { return { valid: ValidationLevel.Drop, matches: [], - node: token, + // @ts-ignore + node: tokens[0], + // @ts-ignore syntax: null, - error: 'expected compound-selector', + error: 'expected compound selector', tokens }; } + length = tokens.length; } - // @ts-ignore - return { - valid: ValidationLevel.Valid, + return match == 0 ? { + valid: ValidationLevel.Drop, matches: [], - node: null, + // @ts-ignore + node: root, + // @ts-ignore syntax: null, - error: '', + error: 'expected compound selector', tokens - }; - } - - const validateSelector$1 = validateComplexSelector; - - function validateRelativeSelector(tokens, root, options) { - tokens = tokens.slice(); - consumeWhitespace(tokens); - if (tokens.length == 0) { + } : // @ts-ignore - return { - valid: ValidationLevel.Drop, + { + valid: ValidationLevel.Valid, matches: [], // @ts-ignore node: root, // @ts-ignore syntax: null, - error: 'expected selector', + error: null, tokens }; - } - // , EnumToken.DescendantCombinatorTokenType + } + + const combinatorsTokens = [exports.EnumToken.ChildCombinatorTokenType, exports.EnumToken.ColumnCombinatorTokenType, + // EnumToken.DescendantCombinatorTokenType, + exports.EnumToken.NextSiblingCombinatorTokenType, exports.EnumToken.SubsequentSiblingCombinatorTokenType]; + // [ ? ]* + function validateComplexSelector(tokens, root, options) { + // [ ? * [ * ]* ]! + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expected selector', + tokens + }; + } + let result = null; + for (const t of splitTokenList(tokens, combinatorsTokens)) { + result = validateCompoundSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + node: root, + syntax: null, + error: 'expecting compound-selector', + tokens + }; + } + + const validateSelector$1 = validateComplexSelector; + + function validateRelativeSelector(tokens, root, options) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected selector', + tokens + }; + } + // , EnumToken.DescendantCombinatorTokenType if (combinatorsTokens.includes(tokens[0].typ)) { tokens.shift(); consumeWhitespace(tokens); @@ -11658,20 +11974,48 @@ } function validateRelativeSelectorList(tokens, root, options) { - let i = 0; - let j = 0; - let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == exports.EnumToken.CommaTokenType) { - result = validateRelativeSelector(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expecting relative selector list', + tokens + }; + } + for (const t of splitTokenList(tokens)) { + if (t.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'unexpected comma', + tokens + }; + } + const result = validateRelativeSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateRelativeSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + return { + valid: ValidationLevel.Valid, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: '', + tokens + }; } function validateComplexSelectorList(tokens, root, options) { @@ -11688,20 +12032,23 @@ tokens }; } - let i = -1; - let j = 0; let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == exports.EnumToken.CommaTokenType) { - result = validateSelector$1(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + for (const t of splitTokenList(tokens)) { + result = validateSelector$1(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateSelector$1(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expecting complex selector list', + tokens + }; } function validateKeyframeSelector(tokens, atRule) { @@ -11852,7 +12199,7 @@ function validateSelector(selector, options, root) { if (root == null) { - return validateRelativeSelectorList(selector, root); + return validateSelectorList(selector, root); } // @ts-ignore if (root.typ == exports.EnumToken.AtRuleNodeType && root.nam.match(/^(-[a-z]+-)?keyframes$/)) { @@ -11880,15 +12227,28 @@ if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + }; + } + let result = null; + const slice = atRule.tokens.slice(); + consumeWhitespace(slice); + if (slice.length == 0) { + return { + valid: ValidationLevel.Valid, matches: [], node: atRule, syntax: '@media', - error: 'expected media query list', + error: '', tokens: [] }; } - const result = validateAtRuleMediaQueryList(atRule.tokens, atRule); + result = validateAtRuleMediaQueryList(atRule.tokens, atRule); if (result.valid == ValidationLevel.Drop) { return result; } @@ -11914,10 +12274,20 @@ }; } function validateAtRuleMediaQueryList(tokenList, atRule) { - for (const tokens of splitTokenList(tokenList)) { + const split = splitTokenList(tokenList); + const matched = []; + let result = null; + let previousToken; + let mediaFeatureType; + for (let i = 0; i < split.length; i++) { + const tokens = split[i].slice(); + const match = []; + result = null; + mediaFeatureType = null; + previousToken = null; if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11925,26 +12295,38 @@ error: 'unexpected token', tokens: [] }; + continue; } - let previousToken = null; while (tokens.length > 0) { - // media-condition - if (validateMediaCondition(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); - } - // media-type - else if (validateMediaFeature(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); + previousToken = tokens[0]; + // media-condition | media-type | custom-media + if (!(validateMediaCondition(tokens[0], atRule) || validateMediaFeature(tokens[0]) || validateCustomMediaCondition(tokens[0], atRule))) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + result = validateAtRuleMediaQueryList(tokens[0].chi, atRule); + } + else { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0] ?? atRule, + syntax: '@media', + error: 'expecting media feature or media condition', + tokens: [] + }; + } + if (result.valid == ValidationLevel.Drop) { + break; + } + result = null; } + match.push(tokens.shift()); if (tokens.length == 0) { break; } if (!consumeWhitespace(tokens)) { if (previousToken?.typ != exports.EnumToken.ParensTokenType) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11952,11 +12334,12 @@ error: 'expected media query list', tokens: [] }; + break; } } - if (![exports.EnumToken.MediaFeatureOrTokenType, exports.EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { + else if (![exports.EnumToken.MediaFeatureOrTokenType, exports.EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11964,31 +12347,70 @@ error: 'expected and/or', tokens: [] }; + break; } - if (tokens.length == 1) { + if (mediaFeatureType == null) { + mediaFeatureType = tokens[0]; + } + if (mediaFeatureType.typ != tokens[0].typ) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected media-condition', + error: 'mixing and/or not allowed at the same level', tokens: [] }; + break; } - tokens.shift(); - if (!consumeWhitespace(tokens)) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }, tokens.shift()); + consumeWhitespace(tokens); + if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected whitespace', + error: 'expected media-condition', tokens: [] }; + break; } + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@media', + error: 'expected media query list', + tokens: [] + }; + } + tokenList.length = 0; + let hasAll = false; + for (let i = 0; i < matched.length; i++) { + if (tokenList.length > 0) { + tokenList.push({ typ: exports.EnumToken.CommaTokenType }); + } + if (matched[i].length == 1 && matched.length > 1 && matched[i][0].typ == exports.EnumToken.MediaFeatureTokenType && matched[i][0].val == 'all') { + hasAll = true; + continue; } + tokenList.push(...matched[i]); + } + if (hasAll && tokenList.length == 0) { + tokenList.push({ typ: exports.EnumToken.MediaFeatureTokenType, val: 'all' }); } // @ts-ignore return { @@ -12000,9 +12422,9 @@ tokens: [] }; } - function validateMediaCondition(token) { + function validateCustomMediaCondition(token, atRule) { if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(token.val); + return validateMediaCondition(token.val, atRule); } if (token.typ != exports.EnumToken.ParensTokenType) { return false; @@ -12011,11 +12433,24 @@ if (chi.length != 1) { return false; } + return chi[0].typ == exports.EnumToken.DashedIdenTokenType; + } + function validateMediaCondition(token, atRule) { + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateMediaCondition(token.val, atRule); + } + if (token.typ != exports.EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports'].includes(token.val))) { + return false; + } + const chi = token.chi.filter((t) => t.typ != exports.EnumToken.CommentTokenType && t.typ != exports.EnumToken.WhitespaceTokenType); + if (chi.length != 1) { + return false; + } if (chi[0].typ == exports.EnumToken.IdenTokenType) { return true; } if (chi[0].typ == exports.EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(chi[0].val); + return validateMediaCondition(chi[0].val, atRule); } if (chi[0].typ == exports.EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == exports.EnumToken.IdenTokenType; @@ -12039,7 +12474,7 @@ matches: [], node: atRule, syntax: '@counter-style', - error: 'expected media query list', + error: 'expected counter style name', tokens: [] }; } @@ -12047,7 +12482,7 @@ if (tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Valid, + valid: ValidationLevel.Drop, matches: [], node: atRule, syntax: '@counter-style', @@ -13647,7 +14082,7 @@ valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports query list', tokens: [] }; @@ -13665,7 +14100,7 @@ valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected at-rule body', tokens: [] }; @@ -13675,7 +14110,7 @@ valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; @@ -13688,7 +14123,7 @@ valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'unexpected token', tokens: [] }; @@ -13722,7 +14157,7 @@ valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? previousToken ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -13734,7 +14169,7 @@ valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected and/or', tokens: [] }; @@ -13745,7 +14180,7 @@ valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports-condition', tokens: [] }; @@ -13757,7 +14192,7 @@ valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -13770,13 +14205,13 @@ if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { return validateSupportCondition(atRule, token.val); } - if (token.typ != exports.EnumToken.ParensTokenType) { + if (token.typ != exports.EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == exports.EnumToken.FunctionTokenType && ['supports', 'font-format', 'font-tech'].includes(token.val))) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], node: token, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports condition-in-parens', tokens: [] }; @@ -13791,7 +14226,7 @@ valid: ValidationLevel.Valid, matches: [], node: null, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; @@ -14310,119 +14745,669 @@ consumeWhitespace(tokens); continue; } - if (tokens[0].typ == exports.EnumToken.FunctionTokenType) { - if (!['url-prefix', 'domain', 'media-document', 'regexp'].some((t) => t.localeCompare(tokens[0].val, undefined, { sensitivity: 'base' }) == 0)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: '@document', - error: 'unexpected token', - tokens - }; + if (tokens[0].typ == exports.EnumToken.FunctionTokenType) { + if (!['url-prefix', 'domain', 'media-document', 'regexp'].some((t) => t.localeCompare(tokens[0].val, undefined, { sensitivity: 'base' }) == 0)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@document', + error: 'unexpected token', + tokens + }; + } + const children = tokens[0].chi.slice(); + consumeWhitespace(children); + if (children.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@document', + error: 'expecting string argument', + tokens + }; + } + if (children[0].typ == exports.EnumToken.StringTokenType) { + children.shift(); + consumeWhitespace(children); + } + if (children.length > 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: children[0], + syntax: '@document', + error: 'unexpected token', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + } + } + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@document', + error: '', + tokens + }; + } + + function validateAtRuleKeyframes(atRule, options, root) { + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@document', + error: 'expecting at-rule prelude', + tokens: [] + }; + } + const tokens = atRule.tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@keyframes', + error: 'expecting at-rule prelude', + tokens + }; + } + if (![exports.EnumToken.StringTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@keyframes', + error: 'expecting ident or string token', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length > 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@keyframes', + error: 'unexpected token', + tokens + }; + } + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@keyframes', + error: '', + tokens + }; + } + + function validateAtRuleWhen(atRule, options, root) { + const slice = Array.isArray(atRule.tokens) ? atRule.tokens.slice() : []; + consumeWhitespace(slice); + if (slice.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: [] + }; + } + const result = validateAtRuleWhenQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@when', + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: result.tokens + }; + } + // media() = media( [ | | ] ) + // supports() = supports( ) + function validateAtRuleWhenQueryList(tokenList, atRule) { + const matched = []; + let result = null; + for (const split of splitTokenList(tokenList)) { + const match = []; + result = null; + consumeWhitespace(split); + if (split.length == 0) { + continue; + } + while (split.length > 0) { + if (split[0].typ != exports.EnumToken.FunctionTokenType || !['media', 'supports', 'font-tech', 'font-format'].includes(split[0].val)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'unexpected token', + tokens: [] + }; + break; + } + const chi = split[0].chi.slice(); + consumeWhitespace(chi); + if (split[0].val == 'media') { + // result = valida + if (chi.length != 1 || !(validateMediaFeature(chi[0]) || validateMediaCondition(split[0], atRule))) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + else if (['supports', 'font-tech', 'font-format'].includes(split[0].val)) { + // result = valida + if (!validateSupportCondition(atRule, split[0])) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + if (match.length > 0) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(split[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting and/or media-condition', + tokens: [] + }; + break; + } + if (match.length > 0) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting media-condition', + tokens: [] + }; + break; + } + } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: result?.node ?? atRule, + syntax: '@when', + error: 'invalid at-rule body', + tokens: [] + }; + } + tokenList.length = 0; + for (const match of matched) { + if (tokenList.length > 0) { + tokenList.push({ + typ: exports.EnumToken.CommaTokenType + }); + } + tokenList.push(...match); + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: tokenList + }; + } + + const validateAtRuleElse = validateAtRuleWhen; + + const validateContainerScrollStateFeature = validateContainerSizeFeature; + function validateAtRuleContainer(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + }; + } + const result = validateAtRuleContainerQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + }; + } + function validateAtRuleContainerQueryList(tokens, atRule) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + let result = null; + let tokenType = null; + for (const queries of splitTokenList(tokens)) { + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + result = null; + const match = []; + let token = null; + tokenType = null; + while (queries.length > 0) { + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + if (queries[0].typ == exports.EnumToken.IdenTokenType) { + match.push(queries.shift()); + consumeWhitespace(queries); + } + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (token.typ != exports.EnumToken.ParensTokenType && (token.typ != exports.EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + if (token.typ == exports.EnumToken.ParensTokenType) { + result = validateContainerSizeFeature(token.chi, atRule); + } + else if (token.val == 'scroll-state') { + result = validateContainerScrollStateFeature(token.chi, atRule); + } + else { + result = validateContainerStyleFeature(token.chi, atRule); + } + if (result.valid == ValidationLevel.Drop) { + return result; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ != exports.EnumToken.MediaFeatureAndTokenType && token.typ != exports.EnumToken.MediaFeatureOrTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = token.typ; + } + if (tokenType != token.typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + function validateContainerStyleFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 1) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + if ([exports.EnumToken.DashedIdenTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ) || + (tokens[0].typ == exports.EnumToken.MediaQueryConditionTokenType && tokens[0].op.typ == exports.EnumToken.ColonTokenType)) { + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + } + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + function validateContainerSizeFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + if (tokens.length == 1) { + const token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateContainerSizeFeature([token.val], atRule); + } + if (token.typ == exports.EnumToken.ParensTokenType) { + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); + } + function validateAtRuleContainerQueryStyleInParams(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + let token = tokens[0]; + let tokenType = null; + let result = null; + while (tokens.length > 0) { + token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (tokens[0].typ != exports.EnumToken.ParensTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + const slices = tokens[0].chi.slice(); + consumeWhitespace(slices); + if (slices.length == 1) { + if ([exports.EnumToken.MediaFeatureNotTokenType, exports.EnumToken.ParensTokenType].includes(slices[0].typ)) { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } } - const children = tokens[0].chi.slice(); - consumeWhitespace(children); - if (children.length == 0) { - // @ts-ignore - return { + else if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + result = { valid: ValidationLevel.Drop, matches: [], - node: tokens[0], - syntax: '@document', - error: 'expecting string argument', + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', tokens }; } - if (children[0].typ == exports.EnumToken.StringTokenType) { - children.shift(); - consumeWhitespace(children); - } - if (children.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: '@document', - error: 'unexpected token', - tokens - }; + } + else { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; } - tokens.shift(); - consumeWhitespace(tokens); + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = tokens[0].typ; + } + if (tokenType != tokens[0].typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; } } - // @ts-ignore return { valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@document', + syntax: '@' + atRule.nam, error: '', tokens }; } - function validateAtRuleKeyframes(atRule, options, root) { + function validateAtRuleCustomMedia(atRule, options, root) { + // media-query-list if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, matches: [], - node: atRule, - syntax: '@document', - error: 'expecting at-rule prelude', + node: null, + syntax: null, + error: '', tokens: [] }; } - const tokens = atRule.tokens.slice(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - // @ts-ignore + const queries = atRule.tokens.slice(); + consumeWhitespace(queries); + if (queries.length == 0 || queries[0].typ != exports.EnumToken.DashedIdenTokenType) { return { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@keyframes', - error: 'expecting at-rule prelude', - tokens + syntax: '@custom-media', + error: 'expecting dashed identifier', + tokens: [] }; } - if (![exports.EnumToken.StringTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ)) { - // @ts-ignore + queries.shift(); + const result = validateAtRuleMediaQueryList(queries, atRule); + if (result.valid == ValidationLevel.Drop) { + atRule.tokens = []; return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@keyframes', - error: 'expecting ident or string token', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: '@keyframes', - error: 'unexpected token', - tokens + syntax: '@custom-media', + error: '', + tokens: [] }; } - // @ts-ignore - return { - valid: ValidationLevel.Valid, - matches: [], - node: atRule, - syntax: '@keyframes', - error: '', - tokens - }; + return result; } function validateAtRule(atRule, options, root) { @@ -14467,9 +15452,21 @@ if (atRule.nam == 'namespace') { return validateAtRuleNamespace(atRule); } + if (atRule.nam == 'when') { + return validateAtRuleWhen(atRule); + } + if (atRule.nam == 'else') { + return validateAtRuleElse(atRule); + } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } + if (atRule.nam == 'custom-media') { + return validateAtRuleCustomMedia(atRule); + } if (['position-try', 'property', 'font-palette-values'].includes(atRule.nam)) { if (!('tokens' in atRule)) { return { @@ -14591,48 +15588,6 @@ exports.EnumToken.StartMatchTokenType, exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType, exports.EnumToken.DashMatchTokenType, exports.EnumToken.ContainMatchTokenType, exports.EnumToken.EOFTokenType ]); - const webkitPseudoAliasMap = { - '-webkit-autofill': 'autofill', - '-webkit-any': 'is', - '-moz-any': 'is', - '-webkit-border-after': 'border-block-end', - '-webkit-border-after-color': 'border-block-end-color', - '-webkit-border-after-style': 'border-block-end-style', - '-webkit-border-after-width': 'border-block-end-width', - '-webkit-border-before': 'border-block-start', - '-webkit-border-before-color': 'border-block-start-color', - '-webkit-border-before-style': 'border-block-start-style', - '-webkit-border-before-width': 'border-block-start-width', - '-webkit-border-end': 'border-inline-end', - '-webkit-border-end-color': 'border-inline-end-color', - '-webkit-border-end-style': 'border-inline-end-style', - '-webkit-border-end-width': 'border-inline-end-width', - '-webkit-border-start': 'border-inline-start', - '-webkit-border-start-color': 'border-inline-start-color', - '-webkit-border-start-style': 'border-inline-start-style', - '-webkit-border-start-width': 'border-inline-start-width', - '-webkit-box-align': 'align-items', - '-webkit-box-direction': 'flex-direction', - '-webkit-box-flex': 'flex-grow', - '-webkit-box-lines': 'flex-flow', - '-webkit-box-ordinal-group': 'order', - '-webkit-box-orient': 'flex-direction', - '-webkit-box-pack': 'justify-content', - '-webkit-column-break-after': 'break-after', - '-webkit-column-break-before': 'break-before', - '-webkit-column-break-inside': 'break-inside', - '-webkit-font-feature-settings': 'font-feature-settings', - '-webkit-hyphenate-character': 'hyphenate-character', - '-webkit-initial-letter': 'initial-letter', - '-webkit-margin-end': 'margin-block-end', - '-webkit-margin-start': 'margin-block-start', - '-webkit-padding-after': 'padding-block-end', - '-webkit-padding-before': 'padding-block-start', - '-webkit-padding-end': 'padding-inline-end', - '-webkit-padding-start': 'padding-inline-start', - '-webkit-min-device-pixel-ratio': 'min-resolution', - '-webkit-max-device-pixel-ratio': 'max-resolution' - }; function reject(reason) { throw new Error(reason ?? 'Parsing aborted'); } @@ -14695,9 +15650,10 @@ } const iter = tokenize$1(iterator); let item; + const rawTokens = []; while (item = iter.next().value) { stats.bytesIn = item.bytesIn; - // + rawTokens.push(item); // doParse error if (item.hint != null && BadTokensTypes.includes(item.hint)) { // bad token @@ -14707,7 +15663,8 @@ tokens.push(item); } if (item.token == ';' || item.token == '{') { - let node = await parseNode(tokens, context, stats, options, errors, src, map); + let node = await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (node != null) { // @ts-ignore stack.push(node); @@ -14733,7 +15690,8 @@ map = new Map; } else if (item.token == '}') { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] ?? ast; @@ -14756,7 +15714,8 @@ } } if (tokens.length > 0) { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (context != null && context.typ == exports.EnumToken.InvalidRuleTokenType) { const index = context.chi.findIndex(node => node == context); if (index > -1) { @@ -14863,7 +15822,17 @@ } }; } - async function parseNode(results, context, stats, options, errors, src, map) { + function getLastNode(context) { + let i = context.chi.length; + while (i--) { + if ([exports.EnumToken.CommentTokenType, exports.EnumToken.CDOCOMMTokenType, exports.EnumToken.WhitespaceTokenType].includes(context.chi[i].typ)) { + continue; + } + return context.chi[i]; + } + return null; + } + async function parseNode(results, context, stats, options, errors, src, map, rawTokens) { let tokens = []; for (const t of results) { const node = getTokenType(t.token, t.hint); @@ -14918,24 +15887,6 @@ if (tokens[0]?.typ == exports.EnumToken.AtRuleTokenType) { const atRule = tokens.shift(); const position = map.get(atRule); - // if (atRule.val == 'charset') { - // - // if (context.typ != EnumToken.StyleSheetNodeType || context.chi.some(t => t.typ != EnumToken.CDOCOMMTokenType && t.typ != EnumToken.CommentNodeType)) { - // - // errors.push({ - // action: 'drop', - // message: 'doParse: invalid @charset', - // location: {src, ...position} - // }); - // - // return null; - // } - // - // if (options.removeCharset) { - // - // return null; - // } - // } // @ts-ignore while ([exports.EnumToken.WhitespaceTokenType].includes(tokens[0]?.typ)) { tokens.shift(); @@ -15032,8 +15983,43 @@ // https://www.w3.org/TR/css-nesting-1/#conditionals // allowed nesting at-rules // there must be a top level rule in the stack - if (atRule.val == 'charset' && options.removeCharset) { - return null; + if (atRule.val == 'charset') { + let spaces = 0; + // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset + for (let k = 1; k < rawTokens.length; k++) { + if (rawTokens[k].hint == exports.EnumToken.WhitespaceTokenType) { + spaces += rawTokens[k].len; + continue; + } + if (rawTokens[k].hint == exports.EnumToken.CommentTokenType) { + continue; + } + if (rawTokens[k].hint == exports.EnumToken.CDOCOMMTokenType) { + continue; + } + if (spaces > 1) { + errors.push({ + action: 'drop', + message: '@charset must have only one space', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + if (rawTokens[k].hint != exports.EnumToken.StringTokenType || rawTokens[k].token[0] != '"') { + errors.push({ + action: 'drop', + message: '@charset expects a ""', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + break; + } + if (options.removeCharset) { + return null; + } } const t = parseAtRulePrelude(parseTokens(tokens, { minify: options.minify }), atRule); const raw = t.reduce((acc, curr) => { @@ -15061,11 +16047,39 @@ node.loc = loc; } if (options.validation) { - const valid = validateAtRule(node, options, context); + let isValid = true; + if (node.nam == 'else') { + const prev = getLastNode(context); + if (prev != null && prev.typ == exports.EnumToken.AtRuleNodeType && ['when', 'else'].includes(prev.nam)) { + if (prev.nam == 'else') { + isValid = Array.isArray(prev.tokens) && prev.tokens.length > 0; + } + } + else { + isValid = false; + } + } + const valid = isValid ? validateAtRule(node, options, context) : { + valid: ValidationLevel.Drop, + node, + matches: [], + syntax: '@' + node.nam, + error: '@' + node.nam + ' not allowed here', + tokens + }; if (valid.valid == ValidationLevel.Drop) { + errors.push({ + action: 'drop', + message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), '') + '"', + // @ts-ignore + location: { src, ...(map.get(valid.node) ?? position) } + }); // @ts-ignore node.typ = exports.EnumToken.InvalidAtRuleTokenType; } + else { + node.val = node.tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false, removeComments: true }), ''); + } } // @ts-ignore context.chi.push(node); @@ -15321,17 +16335,18 @@ continue; } } - if (value.typ == exports.EnumToken.ParensTokenType) { + if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == exports.EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == exports.EnumToken.CommentTokenType || value.chi[i].typ == exports.EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == exports.EnumToken.IdenTokenType) { + if ((dashedIdent && value.chi[i].typ == exports.EnumToken.DashedIdenTokenType) || value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { nameIndex = i; } break; @@ -15360,6 +16375,13 @@ ].includes(value.chi[valueIndex].typ)) { const val = value.chi.splice(valueIndex, 1)[0]; const node = value.chi.splice(nameIndex, 1)[0]; + // 'background' + // @ts-ignore + if (node.typ == exports.EnumToken.ColorTokenType && node.kin == 'dpsys') { + // @ts-ignore + delete node.kin; + node.typ = exports.EnumToken.IdenTokenType; + } while (value.chi[0]?.typ == exports.EnumToken.WhitespaceTokenType) { value.chi.shift(); } @@ -17818,7 +18840,8 @@ continue; } if (node.typ == exports.EnumToken.AtRuleNodeType) { - if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + if (node.nam == 'media' && ['all', '', null].includes(node.val)) { // @ts-ignore ast.chi?.splice(i, 1, ...node.chi); i--; @@ -18133,6 +19156,18 @@ } return ast; } + function hasDeclaration(node) { + // @ts-ignore + for (let i = 0; i < node.chi?.length; i++) { + // @ts-ignore + if (node.chi[i].typ == exports.EnumToken.CommentNodeType) { + continue; + } + // @ts-ignore + return node.chi[i].typ == exports.EnumToken.DeclarationNodeType; + } + return true; + } function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -18233,18 +19268,6 @@ reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } - function hasDeclaration(node) { - // @ts-ignore - for (let i = 0; i < node.chi?.length; i++) { - // @ts-ignore - if (node.chi[i].typ == exports.EnumToken.CommentNodeType) { - continue; - } - // @ts-ignore - return node.chi[i].typ == exports.EnumToken.DeclarationNodeType; - } - return true; - } function splitRule(buffer) { const result = [[]]; let str = ''; diff --git a/dist/index.cjs b/dist/index.cjs index 4fb031ef..663fff7e 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -3332,16 +3332,21 @@ function update(position, str) { } } function doRender(data, options = {}) { + const minify = options.minify ?? true; + const beautify = options.beautify ?? !minify; options = { - ...(options.minify ?? true ? { + ...(beautify ? { + indent: ' ', + newLine: '\n', + } : { indent: '', newLine: '', + }), + ...(minify ? { removeEmpty: true, removeComments: true } : { - indent: ' ', - newLine: '\n', - compress: false, + removeEmpty: false, removeComments: false, }), sourcemap: false, convertColor: true, expandNestingRules: false, preserveLicense: false, ...options }; @@ -3933,6 +3938,341 @@ const mediaTypes = ['all', 'print', 'screen', 'aural', 'braille', 'embossed', 'handheld', 'projection', 'tty', 'tv', 'speech']; // https://www.w3.org/TR/css-values-4/#math-function const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; +const webkitPseudoAliasMap = { + '-webkit-autofill': 'autofill', + '-webkit-any': 'is', + '-moz-any': 'is', + '-webkit-border-after': 'border-block-end', + '-webkit-border-after-color': 'border-block-end-color', + '-webkit-border-after-style': 'border-block-end-style', + '-webkit-border-after-width': 'border-block-end-width', + '-webkit-border-before': 'border-block-start', + '-webkit-border-before-color': 'border-block-start-color', + '-webkit-border-before-style': 'border-block-start-style', + '-webkit-border-before-width': 'border-block-start-width', + '-webkit-border-end': 'border-inline-end', + '-webkit-border-end-color': 'border-inline-end-color', + '-webkit-border-end-style': 'border-inline-end-style', + '-webkit-border-end-width': 'border-inline-end-width', + '-webkit-border-start': 'border-inline-start', + '-webkit-border-start-color': 'border-inline-start-color', + '-webkit-border-start-style': 'border-inline-start-style', + '-webkit-border-start-width': 'border-inline-start-width', + '-webkit-box-align': 'align-items', + '-webkit-box-direction': 'flex-direction', + '-webkit-box-flex': 'flex-grow', + '-webkit-box-lines': 'flex-flow', + '-webkit-box-ordinal-group': 'order', + '-webkit-box-orient': 'flex-direction', + '-webkit-box-pack': 'justify-content', + '-webkit-column-break-after': 'break-after', + '-webkit-column-break-before': 'break-before', + '-webkit-column-break-inside': 'break-inside', + '-webkit-font-feature-settings': 'font-feature-settings', + '-webkit-hyphenate-character': 'hyphenate-character', + '-webkit-initial-letter': 'initial-letter', + '-webkit-margin-end': 'margin-block-end', + '-webkit-margin-start': 'margin-block-start', + '-webkit-padding-after': 'padding-block-end', + '-webkit-padding-before': 'padding-block-start', + '-webkit-padding-end': 'padding-inline-end', + '-webkit-padding-start': 'padding-inline-start', + '-webkit-min-device-pixel-ratio': 'min-resolution', + '-webkit-max-device-pixel-ratio': 'max-resolution' +}; +// https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions +// https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar +const webkitExtensions = new Set([ + '-webkit-app-region', + '-webkit-border-horizontal-spacing', + '-webkit-border-vertical-spacing', + '-webkit-box-reflect', + '-webkit-column-axis', + '-webkit-column-progression', + '-webkit-cursor-visibility', + '-webkit-font-smoothing', + '-webkit-hyphenate-limit-after', + '-webkit-hyphenate-limit-before', + '-webkit-hyphenate-limit-lines', + '-webkit-line-align', + '-webkit-line-box-contain', + '-webkit-line-clamp', + '-webkit-line-grid', + '-webkit-line-snap', + '-webkit-locale', + '-webkit-logical-height', + '-webkit-logical-width', + '-webkit-margin-after', + '-webkit-margin-before', + '-webkit-mask-box-image-outset', + '-webkit-mask-box-image-repeat', + '-webkit-mask-box-image-slice', + '-webkit-mask-box-image-source', + '-webkit-mask-box-image-width', + '-webkit-mask-box-image', + '-webkit-mask-composite', + '-webkit-mask-position-x', + '-webkit-mask-position-y', + '-webkit-mask-repeat-x', + '-webkit-mask-repeat-y', + '-webkit-mask-source-type', + '-webkit-max-logical-height', + '-webkit-max-logical-width', + '-webkit-min-logical-height', + '-webkit-min-logical-width', + '-webkit-nbsp-mode', + '-webkit-perspective-origin-x', + '-webkit-perspective-origin-y', + '-webkit-rtl-ordering', + '-webkit-tap-highlight-color', + '-webkit-text-decoration-skip', + '-webkit-text-decorations-in-effect', + '-webkit-text-fill-color', + '-webkit-text-security', + '-webkit-text-stroke-color', + '-webkit-text-stroke-width', + '-webkit-text-stroke', + '-webkit-text-zoom', + '-webkit-touch-callout', + '-webkit-transform-origin-x', + '-webkit-transform-origin-y', + '-webkit-transform-origin-z', + '-webkit-user-drag', + '-webkit-user-modify', + '-webkit-border-after', + '-webkit-border-after-color', + '-webkit-border-after-style', + '-webkit-border-after-width', + '-webkit-border-before', + '-webkit-border-before-color', + '-webkit-border-before-style', + '-webkit-border-before-width', + '-webkit-border-end', + '-webkit-border-end-color', + '-webkit-border-end-style', + '-webkit-border-end-width', + '-webkit-border-start', + '-webkit-border-start-color', + '-webkit-border-start-style', + '-webkit-border-start-width', + '-webkit-box-align', + '-webkit-box-direction', + '-webkit-box-flex-group', + '-webkit-box-flex', + '-webkit-box-lines', + '-webkit-box-ordinal-group', + '-webkit-box-orient', + '-webkit-box-pack', + '-webkit-column-break-after', + '-webkit-column-break-before', + '-webkit-column-break-inside', + '-webkit-font-feature-settings', + '-webkit-hyphenate-character', + '-webkit-initial-letter', + '-webkit-margin-end', + '-webkit-margin-start', + '-webkit-padding-after', + '-webkit-padding-before', + '-webkit-padding-end', + '-webkit-padding-start', + '-webkit-fill-available', + ':-webkit-animating-full-screen-transition', + ':-webkit-any', + ':-webkit-any-link', + ':-webkit-autofill', + ':-webkit-autofill-strong-password', + ':-webkit-drag', + ':-webkit-full-page-media', + ':-webkit-full-screen*', + ':-webkit-full-screen-ancestor', + ':-webkit-full-screen-document', + ':-webkit-full-screen-controls-hidden', + '::-webkit-file-upload-button*', + '::-webkit-inner-spin-button', + '::-webkit-input-placeholder', + '::-webkit-meter-bar', + '::-webkit-meter-even-less-good-value', + '::-webkit-meter-inner-element', + '::-webkit-meter-optimum-value', + '::-webkit-meter-suboptimum-value', + '::-webkit-progress-bar', + '::-webkit-progress-inner-element', + '::-webkit-progress-value', + '::-webkit-search-cancel-button', + '::-webkit-search-results-button', + '::-webkit-slider-runnable-track', + '::-webkit-slider-thumb', + '-webkit-animation', + '-webkit-device-pixel-ratio', + '-webkit-transform-2d', + '-webkit-transform-3d', + '-webkit-transition', + '::-webkit-scrollbar', + '::-webkit-scrollbar-button', + '::-webkit-scrollbar', + '::-webkit-scrollbar-thumb', + '::-webkit-scrollbar-track', + '::-webkit-scrollbar-track-piece', + '::-webkit-scrollbar:vertical', + '::-webkit-scrollbar-corner ', + '::-webkit-resizer', + ':vertical', + ':horizontal', +]); +// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions +const mozExtensions = new Set([ + '-moz-box-align', + '-moz-box-direction', + '-moz-box-flex', + '-moz-box-ordinal-group', + '-moz-box-orient', + '-moz-box-pack', + '-moz-float-edge', + '-moz-force-broken-image-icon', + '-moz-image-region', + '-moz-orient', + '-moz-osx-font-smoothing', + '-moz-user-focus', + '-moz-user-input', + '-moz-user-modify', + '-moz-animation', + '-moz-animation-delay', + '-moz-animation-direction', + '-moz-animation-duration', + '-moz-animation-fill-mode', + '-moz-animation-iteration-count', + '-moz-animation-name', + '-moz-animation-play-state', + '-moz-animation-timing-function', + '-moz-appearance', + '-moz-backface-visibility', + '-moz-background-clip', + '-moz-background-origin', + '-moz-background-inline-policy', + '-moz-background-size', + '-moz-border-end', + '-moz-border-end-color', + '-moz-border-end-style', + '-moz-border-end-width', + '-moz-border-image', + '-moz-border-start', + '-moz-border-start-color', + '-moz-border-start-style', + '-moz-border-start-width', + '-moz-box-sizing', + 'clip-path', + '-moz-column-count', + '-moz-column-fill', + '-moz-column-gap', + '-moz-column-width', + '-moz-column-rule', + '-moz-column-rule-width', + '-moz-column-rule-style', + '-moz-column-rule-color', + 'filter', + '-moz-font-feature-settings', + '-moz-font-language-override', + '-moz-hyphens', + '-moz-margin-end', + '-moz-margin-start', + 'mask', + '-moz-opacity', + '-moz-outline', + '-moz-outline-color', + '-moz-outline-offset', + '-moz-outline-style', + '-moz-outline-width', + '-moz-padding-end', + '-moz-padding-start', + '-moz-perspective', + '-moz-perspective-origin', + 'pointer-events', + '-moz-tab-size', + '-moz-text-align-last', + '-moz-text-decoration-color', + '-moz-text-decoration-line', + '-moz-text-decoration-style', + '-moz-text-size-adjust', + '-moz-transform', + '-moz-transform-origin', + '-moz-transform-style', + '-moz-transition', + '-moz-transition-delay', + '-moz-transition-duration', + '-moz-transition-property', + '-moz-transition-timing-function', + '-moz-user-select', + '-moz-initial', + '-moz-appearance', + '-moz-linear-gradient', + '-moz-radial-gradient', + '-moz-element', + '-moz-image-rect', + '::-moz-anonymous-block', + '::-moz-anonymous-positioned-block', + ':-moz-any', + ':-moz-any-link', + ':-moz-broken', + '::-moz-canvas', + '::-moz-color-swatch', + '::-moz-cell-content', + ':-moz-drag-over', + ':-moz-first-node', + '::-moz-focus-inner', + '::-moz-focus-outer', + ':-moz-full-screen', + ':-moz-full-screen-ancestor', + ':-moz-handler-blocked', + ':-moz-handler-crashed', + ':-moz-handler-disabled', + '::-moz-inline-table', + ':-moz-last-node', + '::-moz-list-bullet', + '::-moz-list-number', + ':-moz-loading', + ':-moz-locale-dir', + ':-moz-locale-dir', + ':-moz-lwtheme', + ':-moz-lwtheme-brighttext', + ':-moz-lwtheme-darktext', + '::-moz-meter-bar', + ':-moz-native-anonymous', + ':-moz-only-whitespace', + '::-moz-pagebreak', + '::-moz-pagecontent', + ':-moz-placeholder', + '::-moz-placeholder', + '::-moz-progress-bar', + '::-moz-range-progress', + '::-moz-range-thumb', + '::-moz-range-track', + ':-moz-read-only', + ':-moz-read-write', + '::-moz-scrolled-canvas', + '::-moz-scrolled-content', + '::-moz-selection', + ':-moz-submit-invalid', + ':-moz-suppressed', + '::-moz-svg-foreign-content', + '::-moz-table', + '::-moz-table-cell', + '::-moz-table-column', + '::-moz-table-column-group', + '::-moz-table-outer', + '::-moz-table-row', + '::-moz-table-row-group', + ':-moz-ui-invalid', + ':-moz-ui-valid', + ':-moz-user-disabled', + '::-moz-viewport', + '::-moz-viewport-scroll', + ':-moz-window-inactive', + '-moz-device-pixel-ratio', + '-moz-os-version', + '-moz-touch-enabled', + '-moz-windows-glass', + '-moz-alt-content' +]); function isLength(dimension) { return 'unit' in dimension && dimensionUnits.has(dimension.unit.toLowerCase()); } @@ -5953,7 +6293,7 @@ function consumeWhiteSpace(parseInfo) { return count; } function pushToken(token, parseInfo, hint) { - const result = { token, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; + const result = { token, len: parseInfo.currentPosition.ind - parseInfo.position.ind, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; parseInfo.position.ind = parseInfo.currentPosition.ind; parseInfo.position.lin = parseInfo.currentPosition.lin; parseInfo.position.col = Math.max(parseInfo.currentPosition.col, 1); @@ -6121,8 +6461,10 @@ function* tokenize$1(stream) { buffer += value; } } - yield pushToken(buffer, parseInfo, exports.EnumToken.BadCommentTokenType); - buffer = ''; + if (buffer.length > 0) { + yield pushToken(buffer, parseInfo, exports.EnumToken.BadCommentTokenType); + buffer = ''; + } } break; case '&': @@ -11261,12 +11603,12 @@ function consumeWhitespace(tokens) { return true; } -function splitTokenList(tokenList) { +function splitTokenList(tokenList, split = [exports.EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == exports.EnumToken.CommentTokenType) { return acc; } - if (curr.typ == exports.EnumToken.CommaTokenType) { + if (split.includes(curr.typ)) { acc.push([]); } else { @@ -11357,38 +11699,27 @@ function validateFamilyName(tokens, atRule) { }; } -const combinatorsTokens = [exports.EnumToken.ChildCombinatorTokenType, exports.EnumToken.ColumnCombinatorTokenType, - exports.EnumToken.DescendantCombinatorTokenType, exports.EnumToken.NextSiblingCombinatorTokenType, exports.EnumToken.SubsequentSiblingCombinatorTokenType]; -// [ ? ]* -function validateComplexSelector(tokens, root, options) { - // [ ? * [ * ]* ]! - tokens = tokens.slice(); - consumeWhitespace(tokens); +function validateCompoundSelector(tokens, root, options) { if (tokens.length == 0) { + // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], // @ts-ignore node: root, + // @ts-ignore syntax: null, error: 'expected selector', tokens }; } + tokens = tokens.slice(); + consumeWhitespace(tokens); + const config = getSyntaxConfig(); + let match = 0; + let length = tokens.length; while (tokens.length > 0) { - if (combinatorsTokens.includes(tokens[0].typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'unexpected combinator', - tokens - }; - } - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { if (!options?.nestedSelector) { // @ts-ignore return { @@ -11401,105 +11732,120 @@ function validateComplexSelector(tokens, root, options) { tokens }; } - while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - tokens.shift(); - consumeWhitespace(tokens); - } - if (tokens.length == 0) { - break; - } - } - if (exports.EnumToken.IdenTokenType == tokens[0].typ) { + match++; tokens.shift(); consumeWhitespace(tokens); - if (tokens.length == 0) { - break; - } } - if (exports.EnumToken.UniversalSelectorTokenType == tokens[0].typ) { + // + while (tokens.length > 0 && + [ + exports.EnumToken.IdenTokenType, + exports.EnumToken.NameSpaceAttributeTokenType, + exports.EnumToken.ClassSelectorTokenType, + exports.EnumToken.HashTokenType, + exports.EnumToken.UniversalSelectorTokenType + ].includes(tokens[0].typ)) { + match++; tokens.shift(); consumeWhitespace(tokens); } - while (tokens.length > 0) { - if (tokens[0].typ == exports.EnumToken.PseudoClassFuncTokenType) { - if (tokens[0].val.startsWith(':-webkit-')) { + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.PseudoClassFuncTokenType) { + if (!mozExtensions.has(tokens[0].val + '()') && + !webkitExtensions.has(tokens[0].val + '()') && + !((tokens[0].val + '()') in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'invalid pseudo-class', - tokens - }; - } - } - if ([ - exports.EnumToken.ClassSelectorTokenType, - exports.EnumToken.HashTokenType, - exports.EnumToken.PseudoClassTokenType, - exports.EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - tokens.shift(); - consumeWhitespace(tokens); - continue; + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val + '()', + tokens + }; } - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.PseudoClassTokenType) { + const isPseudoElement = tokens[0].val.startsWith('::'); + if ( + // https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions#pseudo-elements + !(isPseudoElement && tokens[0].val.startsWith('::-webkit-')) && + !mozExtensions.has(tokens[0].val) && + !webkitExtensions.has(tokens[0].val) && + !(tokens[0].val in config.selectors) && + !(!isPseudoElement && + (':' + tokens[0].val) in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val, + tokens + }; } - // validate namespace - if (tokens[0].typ == exports.EnumToken.NameSpaceAttributeTokenType) { - if (!((tokens[0].l == null || tokens[0].l.typ == exports.EnumToken.IdenTokenType || (tokens[0].l.typ == exports.EnumToken.LiteralTokenType && tokens[0].l.val == '*')) && - tokens[0].r.typ == exports.EnumToken.IdenTokenType)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting wq-name', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == exports.EnumToken.AttrTokenType) { + const children = tokens[0].chi.slice(); + consumeWhitespace(children); + if (children.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; } - // validate attribute - else if (tokens[0].typ == exports.EnumToken.AttrTokenType) { - const children = tokens[0].chi.slice(); - consumeWhitespace(children); - if (children.length == 0) { - // @ts-ignore + if (![ + exports.EnumToken.IdenTokenType, + exports.EnumToken.NameSpaceAttributeTokenType, + exports.EnumToken.MatchExpressionTokenType + ].includes(children[0].typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + if (children[0].typ == exports.EnumToken.MatchExpressionTokenType) { + if (children.length != 1) { return { valid: ValidationLevel.Drop, matches: [], node: tokens[0], syntax: null, - error: 'invalid attribute selector', + error: 'invalid ', tokens }; } if (![ exports.EnumToken.IdenTokenType, - exports.EnumToken.NameSpaceAttributeTokenType, - exports.EnumToken.MatchExpressionTokenType - ].includes(children[0].typ)) { + exports.EnumToken.NameSpaceAttributeTokenType + ].includes(children[0].l.typ) || + ![ + exports.EnumToken.EqualMatchTokenType, exports.EnumToken.DashMatchTokenType, + exports.EnumToken.StartMatchTokenType, exports.EnumToken.ContainMatchTokenType, + exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType + ].includes(children[0].op.typ) || + ![ + exports.EnumToken.StringTokenType, + exports.EnumToken.IdenTokenType + ].includes(children[0].r.typ)) { // @ts-ignore return { valid: ValidationLevel.Drop, @@ -11510,145 +11856,115 @@ function validateComplexSelector(tokens, root, options) { tokens }; } - if (children[0].typ == exports.EnumToken.MatchExpressionTokenType) { - if (![exports.EnumToken.IdenTokenType, - exports.EnumToken.NameSpaceAttributeTokenType].includes(children[0].l.typ) || - ![ - exports.EnumToken.EqualMatchTokenType, exports.EnumToken.DashMatchTokenType, - exports.EnumToken.StartMatchTokenType, exports.EnumToken.ContainMatchTokenType, - exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType - ].includes(children[0].op.typ) || - ![exports.EnumToken.StringTokenType, - exports.EnumToken.IdenTokenType].includes(children[0].r.typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - } - children.shift(); - consumeWhitespace(children); - if (children.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: null, - error: 'unexpected token', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - break; - } - if (tokens.length == 0) { - break; - } - // combinator - if (!combinatorsTokens.includes(tokens[0].typ)) { - if (tokens[0].typ == exports.EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { + if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], - // @ts-ignore node: tokens[0], syntax: null, - error: 'nested selector not allowed', + error: 'invalid attribute selector', tokens }; } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - if (tokens.length > 0 && - [ - exports.EnumToken.IdenTokenType, - exports.EnumToken.AttrTokenType, - exports.EnumToken.NameSpaceAttributeTokenType, - exports.EnumToken.ClassSelectorTokenType, - exports.EnumToken.HashTokenType, - exports.EnumToken.PseudoClassTokenType, - exports.EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - continue; } - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting combinator or subclass-selector', - tokens - }; + match++; + tokens.shift(); + consumeWhitespace(tokens); } - const token = tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - // @ts-ignore + if (length == tokens.length) { return { valid: ValidationLevel.Drop, matches: [], - node: token, + // @ts-ignore + node: tokens[0], + // @ts-ignore syntax: null, - error: 'expected compound-selector', + error: 'expected compound selector', tokens }; } + length = tokens.length; } - // @ts-ignore - return { - valid: ValidationLevel.Valid, + return match == 0 ? { + valid: ValidationLevel.Drop, matches: [], - node: null, + // @ts-ignore + node: root, + // @ts-ignore syntax: null, - error: '', + error: 'expected compound selector', tokens - }; -} - -const validateSelector$1 = validateComplexSelector; - -function validateRelativeSelector(tokens, root, options) { - tokens = tokens.slice(); - consumeWhitespace(tokens); - if (tokens.length == 0) { + } : // @ts-ignore - return { - valid: ValidationLevel.Drop, + { + valid: ValidationLevel.Valid, matches: [], // @ts-ignore node: root, // @ts-ignore syntax: null, - error: 'expected selector', + error: null, tokens }; - } - // , EnumToken.DescendantCombinatorTokenType +} + +const combinatorsTokens = [exports.EnumToken.ChildCombinatorTokenType, exports.EnumToken.ColumnCombinatorTokenType, + // EnumToken.DescendantCombinatorTokenType, + exports.EnumToken.NextSiblingCombinatorTokenType, exports.EnumToken.SubsequentSiblingCombinatorTokenType]; +// [ ? ]* +function validateComplexSelector(tokens, root, options) { + // [ ? * [ * ]* ]! + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expected selector', + tokens + }; + } + let result = null; + for (const t of splitTokenList(tokens, combinatorsTokens)) { + result = validateCompoundSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + node: root, + syntax: null, + error: 'expecting compound-selector', + tokens + }; +} + +const validateSelector$1 = validateComplexSelector; + +function validateRelativeSelector(tokens, root, options) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected selector', + tokens + }; + } + // , EnumToken.DescendantCombinatorTokenType if (combinatorsTokens.includes(tokens[0].typ)) { tokens.shift(); consumeWhitespace(tokens); @@ -11657,20 +11973,48 @@ function validateRelativeSelector(tokens, root, options) { } function validateRelativeSelectorList(tokens, root, options) { - let i = 0; - let j = 0; - let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == exports.EnumToken.CommaTokenType) { - result = validateRelativeSelector(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expecting relative selector list', + tokens + }; + } + for (const t of splitTokenList(tokens)) { + if (t.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'unexpected comma', + tokens + }; + } + const result = validateRelativeSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateRelativeSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + return { + valid: ValidationLevel.Valid, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: '', + tokens + }; } function validateComplexSelectorList(tokens, root, options) { @@ -11687,20 +12031,23 @@ function validateComplexSelectorList(tokens, root, options) { tokens }; } - let i = -1; - let j = 0; let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == exports.EnumToken.CommaTokenType) { - result = validateSelector$1(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + for (const t of splitTokenList(tokens)) { + result = validateSelector$1(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateSelector$1(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expecting complex selector list', + tokens + }; } function validateKeyframeSelector(tokens, atRule) { @@ -11851,7 +12198,7 @@ const validateSelectorList = validateComplexSelectorList; function validateSelector(selector, options, root) { if (root == null) { - return validateRelativeSelectorList(selector, root); + return validateSelectorList(selector, root); } // @ts-ignore if (root.typ == exports.EnumToken.AtRuleNodeType && root.nam.match(/^(-[a-z]+-)?keyframes$/)) { @@ -11879,15 +12226,28 @@ function validateAtRuleMedia(atRule, options, root) { if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + }; + } + let result = null; + const slice = atRule.tokens.slice(); + consumeWhitespace(slice); + if (slice.length == 0) { + return { + valid: ValidationLevel.Valid, matches: [], node: atRule, syntax: '@media', - error: 'expected media query list', + error: '', tokens: [] }; } - const result = validateAtRuleMediaQueryList(atRule.tokens, atRule); + result = validateAtRuleMediaQueryList(atRule.tokens, atRule); if (result.valid == ValidationLevel.Drop) { return result; } @@ -11913,10 +12273,20 @@ function validateAtRuleMedia(atRule, options, root) { }; } function validateAtRuleMediaQueryList(tokenList, atRule) { - for (const tokens of splitTokenList(tokenList)) { + const split = splitTokenList(tokenList); + const matched = []; + let result = null; + let previousToken; + let mediaFeatureType; + for (let i = 0; i < split.length; i++) { + const tokens = split[i].slice(); + const match = []; + result = null; + mediaFeatureType = null; + previousToken = null; if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11924,26 +12294,38 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'unexpected token', tokens: [] }; + continue; } - let previousToken = null; while (tokens.length > 0) { - // media-condition - if (validateMediaCondition(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); - } - // media-type - else if (validateMediaFeature(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); + previousToken = tokens[0]; + // media-condition | media-type | custom-media + if (!(validateMediaCondition(tokens[0], atRule) || validateMediaFeature(tokens[0]) || validateCustomMediaCondition(tokens[0], atRule))) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + result = validateAtRuleMediaQueryList(tokens[0].chi, atRule); + } + else { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0] ?? atRule, + syntax: '@media', + error: 'expecting media feature or media condition', + tokens: [] + }; + } + if (result.valid == ValidationLevel.Drop) { + break; + } + result = null; } + match.push(tokens.shift()); if (tokens.length == 0) { break; } if (!consumeWhitespace(tokens)) { if (previousToken?.typ != exports.EnumToken.ParensTokenType) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11951,11 +12333,12 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'expected media query list', tokens: [] }; + break; } } - if (![exports.EnumToken.MediaFeatureOrTokenType, exports.EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { + else if (![exports.EnumToken.MediaFeatureOrTokenType, exports.EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -11963,31 +12346,70 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'expected and/or', tokens: [] }; + break; } - if (tokens.length == 1) { + if (mediaFeatureType == null) { + mediaFeatureType = tokens[0]; + } + if (mediaFeatureType.typ != tokens[0].typ) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected media-condition', + error: 'mixing and/or not allowed at the same level', tokens: [] }; + break; } - tokens.shift(); - if (!consumeWhitespace(tokens)) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }, tokens.shift()); + consumeWhitespace(tokens); + if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected whitespace', + error: 'expected media-condition', tokens: [] }; + break; } + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@media', + error: 'expected media query list', + tokens: [] + }; + } + tokenList.length = 0; + let hasAll = false; + for (let i = 0; i < matched.length; i++) { + if (tokenList.length > 0) { + tokenList.push({ typ: exports.EnumToken.CommaTokenType }); + } + if (matched[i].length == 1 && matched.length > 1 && matched[i][0].typ == exports.EnumToken.MediaFeatureTokenType && matched[i][0].val == 'all') { + hasAll = true; + continue; } + tokenList.push(...matched[i]); + } + if (hasAll && tokenList.length == 0) { + tokenList.push({ typ: exports.EnumToken.MediaFeatureTokenType, val: 'all' }); } // @ts-ignore return { @@ -11999,9 +12421,9 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { tokens: [] }; } -function validateMediaCondition(token) { +function validateCustomMediaCondition(token, atRule) { if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(token.val); + return validateMediaCondition(token.val, atRule); } if (token.typ != exports.EnumToken.ParensTokenType) { return false; @@ -12010,11 +12432,24 @@ function validateMediaCondition(token) { if (chi.length != 1) { return false; } + return chi[0].typ == exports.EnumToken.DashedIdenTokenType; +} +function validateMediaCondition(token, atRule) { + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateMediaCondition(token.val, atRule); + } + if (token.typ != exports.EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports'].includes(token.val))) { + return false; + } + const chi = token.chi.filter((t) => t.typ != exports.EnumToken.CommentTokenType && t.typ != exports.EnumToken.WhitespaceTokenType); + if (chi.length != 1) { + return false; + } if (chi[0].typ == exports.EnumToken.IdenTokenType) { return true; } if (chi[0].typ == exports.EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(chi[0].val); + return validateMediaCondition(chi[0].val, atRule); } if (chi[0].typ == exports.EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == exports.EnumToken.IdenTokenType; @@ -12038,7 +12473,7 @@ function validateAtRuleCounterStyle(atRule, options, root) { matches: [], node: atRule, syntax: '@counter-style', - error: 'expected media query list', + error: 'expected counter style name', tokens: [] }; } @@ -12046,7 +12481,7 @@ function validateAtRuleCounterStyle(atRule, options, root) { if (tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Valid, + valid: ValidationLevel.Drop, matches: [], node: atRule, syntax: '@counter-style', @@ -13646,7 +14081,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports query list', tokens: [] }; @@ -13664,7 +14099,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected at-rule body', tokens: [] }; @@ -13674,7 +14109,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; @@ -13687,7 +14122,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'unexpected token', tokens: [] }; @@ -13721,7 +14156,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? previousToken ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -13733,7 +14168,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected and/or', tokens: [] }; @@ -13744,7 +14179,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports-condition', tokens: [] }; @@ -13756,7 +14191,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -13769,13 +14204,13 @@ function validateSupportCondition(atRule, token) { if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { return validateSupportCondition(atRule, token.val); } - if (token.typ != exports.EnumToken.ParensTokenType) { + if (token.typ != exports.EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == exports.EnumToken.FunctionTokenType && ['supports', 'font-format', 'font-tech'].includes(token.val))) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], node: token, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports condition-in-parens', tokens: [] }; @@ -13790,7 +14225,7 @@ function validateSupportCondition(atRule, token) { valid: ValidationLevel.Valid, matches: [], node: null, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; @@ -14309,119 +14744,669 @@ function validateAtRuleDocument(atRule, options, root) { consumeWhitespace(tokens); continue; } - if (tokens[0].typ == exports.EnumToken.FunctionTokenType) { - if (!['url-prefix', 'domain', 'media-document', 'regexp'].some((t) => t.localeCompare(tokens[0].val, undefined, { sensitivity: 'base' }) == 0)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: '@document', - error: 'unexpected token', - tokens - }; + if (tokens[0].typ == exports.EnumToken.FunctionTokenType) { + if (!['url-prefix', 'domain', 'media-document', 'regexp'].some((t) => t.localeCompare(tokens[0].val, undefined, { sensitivity: 'base' }) == 0)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@document', + error: 'unexpected token', + tokens + }; + } + const children = tokens[0].chi.slice(); + consumeWhitespace(children); + if (children.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@document', + error: 'expecting string argument', + tokens + }; + } + if (children[0].typ == exports.EnumToken.StringTokenType) { + children.shift(); + consumeWhitespace(children); + } + if (children.length > 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: children[0], + syntax: '@document', + error: 'unexpected token', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + } + } + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@document', + error: '', + tokens + }; +} + +function validateAtRuleKeyframes(atRule, options, root) { + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@document', + error: 'expecting at-rule prelude', + tokens: [] + }; + } + const tokens = atRule.tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@keyframes', + error: 'expecting at-rule prelude', + tokens + }; + } + if (![exports.EnumToken.StringTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@keyframes', + error: 'expecting ident or string token', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length > 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@keyframes', + error: 'unexpected token', + tokens + }; + } + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@keyframes', + error: '', + tokens + }; +} + +function validateAtRuleWhen(atRule, options, root) { + const slice = Array.isArray(atRule.tokens) ? atRule.tokens.slice() : []; + consumeWhitespace(slice); + if (slice.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: [] + }; + } + const result = validateAtRuleWhenQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@when', + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: result.tokens + }; +} +// media() = media( [ | | ] ) +// supports() = supports( ) +function validateAtRuleWhenQueryList(tokenList, atRule) { + const matched = []; + let result = null; + for (const split of splitTokenList(tokenList)) { + const match = []; + result = null; + consumeWhitespace(split); + if (split.length == 0) { + continue; + } + while (split.length > 0) { + if (split[0].typ != exports.EnumToken.FunctionTokenType || !['media', 'supports', 'font-tech', 'font-format'].includes(split[0].val)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'unexpected token', + tokens: [] + }; + break; + } + const chi = split[0].chi.slice(); + consumeWhitespace(chi); + if (split[0].val == 'media') { + // result = valida + if (chi.length != 1 || !(validateMediaFeature(chi[0]) || validateMediaCondition(split[0], atRule))) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + else if (['supports', 'font-tech', 'font-format'].includes(split[0].val)) { + // result = valida + if (!validateSupportCondition(atRule, split[0])) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + if (match.length > 0) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(split[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting and/or media-condition', + tokens: [] + }; + break; + } + if (match.length > 0) { + match.push({ typ: exports.EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting media-condition', + tokens: [] + }; + break; + } + } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: result?.node ?? atRule, + syntax: '@when', + error: 'invalid at-rule body', + tokens: [] + }; + } + tokenList.length = 0; + for (const match of matched) { + if (tokenList.length > 0) { + tokenList.push({ + typ: exports.EnumToken.CommaTokenType + }); + } + tokenList.push(...match); + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: tokenList + }; +} + +const validateAtRuleElse = validateAtRuleWhen; + +const validateContainerScrollStateFeature = validateContainerSizeFeature; +function validateAtRuleContainer(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + }; + } + const result = validateAtRuleContainerQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + }; +} +function validateAtRuleContainerQueryList(tokens, atRule) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + let result = null; + let tokenType = null; + for (const queries of splitTokenList(tokens)) { + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + result = null; + const match = []; + let token = null; + tokenType = null; + while (queries.length > 0) { + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + if (queries[0].typ == exports.EnumToken.IdenTokenType) { + match.push(queries.shift()); + consumeWhitespace(queries); + } + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (token.typ != exports.EnumToken.ParensTokenType && (token.typ != exports.EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + if (token.typ == exports.EnumToken.ParensTokenType) { + result = validateContainerSizeFeature(token.chi, atRule); + } + else if (token.val == 'scroll-state') { + result = validateContainerScrollStateFeature(token.chi, atRule); + } + else { + result = validateContainerStyleFeature(token.chi, atRule); + } + if (result.valid == ValidationLevel.Drop) { + return result; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ != exports.EnumToken.MediaFeatureAndTokenType && token.typ != exports.EnumToken.MediaFeatureOrTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = token.typ; + } + if (tokenType != token.typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; +} +function validateContainerStyleFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 1) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + if ([exports.EnumToken.DashedIdenTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ) || + (tokens[0].typ == exports.EnumToken.MediaQueryConditionTokenType && tokens[0].op.typ == exports.EnumToken.ColonTokenType)) { + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + } + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; +} +function validateContainerSizeFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + if (tokens.length == 1) { + const token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateContainerSizeFeature([token.val], atRule); + } + if (token.typ == exports.EnumToken.ParensTokenType) { + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); +} +function validateAtRuleContainerQueryStyleInParams(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + let token = tokens[0]; + let tokenType = null; + let result = null; + while (tokens.length > 0) { + token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (tokens[0].typ != exports.EnumToken.ParensTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + const slices = tokens[0].chi.slice(); + consumeWhitespace(slices); + if (slices.length == 1) { + if ([exports.EnumToken.MediaFeatureNotTokenType, exports.EnumToken.ParensTokenType].includes(slices[0].typ)) { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } } - const children = tokens[0].chi.slice(); - consumeWhitespace(children); - if (children.length == 0) { - // @ts-ignore - return { + else if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + result = { valid: ValidationLevel.Drop, matches: [], - node: tokens[0], - syntax: '@document', - error: 'expecting string argument', + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', tokens }; } - if (children[0].typ == exports.EnumToken.StringTokenType) { - children.shift(); - consumeWhitespace(children); - } - if (children.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: '@document', - error: 'unexpected token', - tokens - }; + } + else { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; } - tokens.shift(); - consumeWhitespace(tokens); + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = tokens[0].typ; + } + if (tokenType != tokens[0].typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; } } - // @ts-ignore return { valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@document', + syntax: '@' + atRule.nam, error: '', tokens }; } -function validateAtRuleKeyframes(atRule, options, root) { +function validateAtRuleCustomMedia(atRule, options, root) { + // media-query-list if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, matches: [], - node: atRule, - syntax: '@document', - error: 'expecting at-rule prelude', + node: null, + syntax: null, + error: '', tokens: [] }; } - const tokens = atRule.tokens.slice(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - // @ts-ignore + const queries = atRule.tokens.slice(); + consumeWhitespace(queries); + if (queries.length == 0 || queries[0].typ != exports.EnumToken.DashedIdenTokenType) { return { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@keyframes', - error: 'expecting at-rule prelude', - tokens + syntax: '@custom-media', + error: 'expecting dashed identifier', + tokens: [] }; } - if (![exports.EnumToken.StringTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ)) { - // @ts-ignore + queries.shift(); + const result = validateAtRuleMediaQueryList(queries, atRule); + if (result.valid == ValidationLevel.Drop) { + atRule.tokens = []; return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@keyframes', - error: 'expecting ident or string token', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: '@keyframes', - error: 'unexpected token', - tokens + syntax: '@custom-media', + error: '', + tokens: [] }; } - // @ts-ignore - return { - valid: ValidationLevel.Valid, - matches: [], - node: atRule, - syntax: '@keyframes', - error: '', - tokens - }; + return result; } function validateAtRule(atRule, options, root) { @@ -14466,9 +15451,21 @@ function validateAtRule(atRule, options, root) { if (atRule.nam == 'namespace') { return validateAtRuleNamespace(atRule); } + if (atRule.nam == 'when') { + return validateAtRuleWhen(atRule); + } + if (atRule.nam == 'else') { + return validateAtRuleElse(atRule); + } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } + if (atRule.nam == 'custom-media') { + return validateAtRuleCustomMedia(atRule); + } if (['position-try', 'property', 'font-palette-values'].includes(atRule.nam)) { if (!('tokens' in atRule)) { return { @@ -14590,48 +15587,6 @@ const enumTokenHints = new Set([ exports.EnumToken.StartMatchTokenType, exports.EnumToken.EndMatchTokenType, exports.EnumToken.IncludeMatchTokenType, exports.EnumToken.DashMatchTokenType, exports.EnumToken.ContainMatchTokenType, exports.EnumToken.EOFTokenType ]); -const webkitPseudoAliasMap = { - '-webkit-autofill': 'autofill', - '-webkit-any': 'is', - '-moz-any': 'is', - '-webkit-border-after': 'border-block-end', - '-webkit-border-after-color': 'border-block-end-color', - '-webkit-border-after-style': 'border-block-end-style', - '-webkit-border-after-width': 'border-block-end-width', - '-webkit-border-before': 'border-block-start', - '-webkit-border-before-color': 'border-block-start-color', - '-webkit-border-before-style': 'border-block-start-style', - '-webkit-border-before-width': 'border-block-start-width', - '-webkit-border-end': 'border-inline-end', - '-webkit-border-end-color': 'border-inline-end-color', - '-webkit-border-end-style': 'border-inline-end-style', - '-webkit-border-end-width': 'border-inline-end-width', - '-webkit-border-start': 'border-inline-start', - '-webkit-border-start-color': 'border-inline-start-color', - '-webkit-border-start-style': 'border-inline-start-style', - '-webkit-border-start-width': 'border-inline-start-width', - '-webkit-box-align': 'align-items', - '-webkit-box-direction': 'flex-direction', - '-webkit-box-flex': 'flex-grow', - '-webkit-box-lines': 'flex-flow', - '-webkit-box-ordinal-group': 'order', - '-webkit-box-orient': 'flex-direction', - '-webkit-box-pack': 'justify-content', - '-webkit-column-break-after': 'break-after', - '-webkit-column-break-before': 'break-before', - '-webkit-column-break-inside': 'break-inside', - '-webkit-font-feature-settings': 'font-feature-settings', - '-webkit-hyphenate-character': 'hyphenate-character', - '-webkit-initial-letter': 'initial-letter', - '-webkit-margin-end': 'margin-block-end', - '-webkit-margin-start': 'margin-block-start', - '-webkit-padding-after': 'padding-block-end', - '-webkit-padding-before': 'padding-block-start', - '-webkit-padding-end': 'padding-inline-end', - '-webkit-padding-start': 'padding-inline-start', - '-webkit-min-device-pixel-ratio': 'min-resolution', - '-webkit-max-device-pixel-ratio': 'max-resolution' -}; function reject(reason) { throw new Error(reason ?? 'Parsing aborted'); } @@ -14694,9 +15649,10 @@ async function doParse(iterator, options = {}) { } const iter = tokenize$1(iterator); let item; + const rawTokens = []; while (item = iter.next().value) { stats.bytesIn = item.bytesIn; - // + rawTokens.push(item); // doParse error if (item.hint != null && BadTokensTypes.includes(item.hint)) { // bad token @@ -14706,7 +15662,8 @@ async function doParse(iterator, options = {}) { tokens.push(item); } if (item.token == ';' || item.token == '{') { - let node = await parseNode(tokens, context, stats, options, errors, src, map); + let node = await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (node != null) { // @ts-ignore stack.push(node); @@ -14732,7 +15689,8 @@ async function doParse(iterator, options = {}) { map = new Map; } else if (item.token == '}') { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] ?? ast; @@ -14755,7 +15713,8 @@ async function doParse(iterator, options = {}) { } } if (tokens.length > 0) { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (context != null && context.typ == exports.EnumToken.InvalidRuleTokenType) { const index = context.chi.findIndex(node => node == context); if (index > -1) { @@ -14862,7 +15821,17 @@ async function doParse(iterator, options = {}) { } }; } -async function parseNode(results, context, stats, options, errors, src, map) { +function getLastNode(context) { + let i = context.chi.length; + while (i--) { + if ([exports.EnumToken.CommentTokenType, exports.EnumToken.CDOCOMMTokenType, exports.EnumToken.WhitespaceTokenType].includes(context.chi[i].typ)) { + continue; + } + return context.chi[i]; + } + return null; +} +async function parseNode(results, context, stats, options, errors, src, map, rawTokens) { let tokens = []; for (const t of results) { const node = getTokenType(t.token, t.hint); @@ -14917,24 +15886,6 @@ async function parseNode(results, context, stats, options, errors, src, map) { if (tokens[0]?.typ == exports.EnumToken.AtRuleTokenType) { const atRule = tokens.shift(); const position = map.get(atRule); - // if (atRule.val == 'charset') { - // - // if (context.typ != EnumToken.StyleSheetNodeType || context.chi.some(t => t.typ != EnumToken.CDOCOMMTokenType && t.typ != EnumToken.CommentNodeType)) { - // - // errors.push({ - // action: 'drop', - // message: 'doParse: invalid @charset', - // location: {src, ...position} - // }); - // - // return null; - // } - // - // if (options.removeCharset) { - // - // return null; - // } - // } // @ts-ignore while ([exports.EnumToken.WhitespaceTokenType].includes(tokens[0]?.typ)) { tokens.shift(); @@ -15031,8 +15982,43 @@ async function parseNode(results, context, stats, options, errors, src, map) { // https://www.w3.org/TR/css-nesting-1/#conditionals // allowed nesting at-rules // there must be a top level rule in the stack - if (atRule.val == 'charset' && options.removeCharset) { - return null; + if (atRule.val == 'charset') { + let spaces = 0; + // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset + for (let k = 1; k < rawTokens.length; k++) { + if (rawTokens[k].hint == exports.EnumToken.WhitespaceTokenType) { + spaces += rawTokens[k].len; + continue; + } + if (rawTokens[k].hint == exports.EnumToken.CommentTokenType) { + continue; + } + if (rawTokens[k].hint == exports.EnumToken.CDOCOMMTokenType) { + continue; + } + if (spaces > 1) { + errors.push({ + action: 'drop', + message: '@charset must have only one space', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + if (rawTokens[k].hint != exports.EnumToken.StringTokenType || rawTokens[k].token[0] != '"') { + errors.push({ + action: 'drop', + message: '@charset expects a ""', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + break; + } + if (options.removeCharset) { + return null; + } } const t = parseAtRulePrelude(parseTokens(tokens, { minify: options.minify }), atRule); const raw = t.reduce((acc, curr) => { @@ -15060,11 +16046,39 @@ async function parseNode(results, context, stats, options, errors, src, map) { node.loc = loc; } if (options.validation) { - const valid = validateAtRule(node, options, context); + let isValid = true; + if (node.nam == 'else') { + const prev = getLastNode(context); + if (prev != null && prev.typ == exports.EnumToken.AtRuleNodeType && ['when', 'else'].includes(prev.nam)) { + if (prev.nam == 'else') { + isValid = Array.isArray(prev.tokens) && prev.tokens.length > 0; + } + } + else { + isValid = false; + } + } + const valid = isValid ? validateAtRule(node, options, context) : { + valid: ValidationLevel.Drop, + node, + matches: [], + syntax: '@' + node.nam, + error: '@' + node.nam + ' not allowed here', + tokens + }; if (valid.valid == ValidationLevel.Drop) { + errors.push({ + action: 'drop', + message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), '') + '"', + // @ts-ignore + location: { src, ...(map.get(valid.node) ?? position) } + }); // @ts-ignore node.typ = exports.EnumToken.InvalidAtRuleTokenType; } + else { + node.val = node.tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false, removeComments: true }), ''); + } } // @ts-ignore context.chi.push(node); @@ -15320,17 +16334,18 @@ function parseAtRulePrelude(tokens, atRule) { continue; } } - if (value.typ == exports.EnumToken.ParensTokenType) { + if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == exports.EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == exports.EnumToken.CommentTokenType || value.chi[i].typ == exports.EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == exports.EnumToken.IdenTokenType) { + if ((dashedIdent && value.chi[i].typ == exports.EnumToken.DashedIdenTokenType) || value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { nameIndex = i; } break; @@ -15359,6 +16374,13 @@ function parseAtRulePrelude(tokens, atRule) { ].includes(value.chi[valueIndex].typ)) { const val = value.chi.splice(valueIndex, 1)[0]; const node = value.chi.splice(nameIndex, 1)[0]; + // 'background' + // @ts-ignore + if (node.typ == exports.EnumToken.ColorTokenType && node.kin == 'dpsys') { + // @ts-ignore + delete node.kin; + node.typ = exports.EnumToken.IdenTokenType; + } while (value.chi[0]?.typ == exports.EnumToken.WhitespaceTokenType) { value.chi.shift(); } @@ -17817,7 +18839,8 @@ function minify(ast, options = {}, recursive = false, errors, nestingContent, co continue; } if (node.typ == exports.EnumToken.AtRuleNodeType) { - if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + if (node.nam == 'media' && ['all', '', null].includes(node.val)) { // @ts-ignore ast.chi?.splice(i, 1, ...node.chi); i--; @@ -18132,6 +19155,18 @@ function minify(ast, options = {}, recursive = false, errors, nestingContent, co } return ast; } +function hasDeclaration(node) { + // @ts-ignore + for (let i = 0; i < node.chi?.length; i++) { + // @ts-ignore + if (node.chi[i].typ == exports.EnumToken.CommentNodeType) { + continue; + } + // @ts-ignore + return node.chi[i].typ == exports.EnumToken.DeclarationNodeType; + } + return true; +} function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -18232,18 +19267,6 @@ function reduceSelector(selector) { reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } -function hasDeclaration(node) { - // @ts-ignore - for (let i = 0; i < node.chi?.length; i++) { - // @ts-ignore - if (node.chi[i].typ == exports.EnumToken.CommentNodeType) { - continue; - } - // @ts-ignore - return node.chi[i].typ == exports.EnumToken.DeclarationNodeType; - } - return true; -} function splitRule(buffer) { const result = [[]]; let str = ''; diff --git a/dist/index.d.ts b/dist/index.d.ts index 056675ea..22299448 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -567,21 +567,15 @@ export declare interface MediaFeatureToken extends BaseToken { val: string; } -export declare interface MediaFeatureOnlyToken extends BaseToken { - - typ: EnumToken.MediaFeatureOnlyTokenType, - val: Token; -} - export declare interface MediaFeatureNotToken extends BaseToken { typ: EnumToken.MediaFeatureNotTokenType, val: Token; } -export declare interface MediaFeatureNotToken extends BaseToken { +export declare interface MediaFeatureOnlyToken extends BaseToken { - typ: EnumToken.MediaFeatureNotTokenType, + typ: EnumToken.MediaFeatureOnlyTokenType, val: Token; } @@ -861,6 +855,7 @@ export declare interface AstKeyFrameRule extends BaseToken { chi: Array; optimized?: OptimizedSelector; raw?: RawSelectorTokens; + tokens?: Token[] } export declare type RawSelectorTokens = string[][]; @@ -1060,6 +1055,7 @@ export declare interface ResolvedPath { export declare interface RenderOptions { minify?: boolean; + beautify?: boolean; removeEmpty?: boolean; expandNestingRules?: boolean; preserveLicense?: boolean; @@ -1073,7 +1069,6 @@ export declare interface RenderOptions { cwd?: string; load?: (url: string, currentUrl: string) => Promise; resolve?: (url: string, currentUrl: string, currentWorkingDirectory?: string) => ResolvedPath; - } export declare interface TransformOptions extends ParserOptions, RenderOptions { diff --git a/dist/lib/ast/minify.js b/dist/lib/ast/minify.js index 75daf842..5a6de6f7 100644 --- a/dist/lib/ast/minify.js +++ b/dist/lib/ast/minify.js @@ -82,7 +82,8 @@ function minify(ast, options = {}, recursive = false, errors, nestingContent, co continue; } if (node.typ == EnumToken.AtRuleNodeType) { - if (node.nam == 'media' && node.val == 'all') { + // @ts-ignore + if (node.nam == 'media' && ['all', '', null].includes(node.val)) { // @ts-ignore ast.chi?.splice(i, 1, ...node.chi); i--; @@ -397,6 +398,18 @@ function minify(ast, options = {}, recursive = false, errors, nestingContent, co } return ast; } +function hasDeclaration(node) { + // @ts-ignore + for (let i = 0; i < node.chi?.length; i++) { + // @ts-ignore + if (node.chi[i].typ == EnumToken.CommentNodeType) { + continue; + } + // @ts-ignore + return node.chi[i].typ == EnumToken.DeclarationNodeType; + } + return true; +} function reduceSelector(selector) { if (selector.length == 0) { return null; @@ -497,18 +510,6 @@ function reduceSelector(selector) { reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } -function hasDeclaration(node) { - // @ts-ignore - for (let i = 0; i < node.chi?.length; i++) { - // @ts-ignore - if (node.chi[i].typ == EnumToken.CommentNodeType) { - continue; - } - // @ts-ignore - return node.chi[i].typ == EnumToken.DeclarationNodeType; - } - return true; -} function splitRule(buffer) { const result = [[]]; let str = ''; diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index 8407a344..3d25a482 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -1,4 +1,4 @@ -import { isPseudo, isAtKeyword, isFunction, isNumber, isPercentage, isFlex, isDimension, parseDimension, isIdent, isHexColor, isHash, isIdentStart, mathFuncs, isColor, mediaTypes } from '../syntax/syntax.js'; +import { isPseudo, isAtKeyword, isFunction, isNumber, isPercentage, isFlex, isDimension, parseDimension, isIdent, isHexColor, isHash, webkitPseudoAliasMap, isIdentStart, mathFuncs, isColor, mediaTypes } from '../syntax/syntax.js'; import './utils/config.js'; import { EnumToken, funcLike, ValidationLevel } from '../ast/types.js'; import { minify, definedPropertySettings, combinators } from '../ast/minify.js'; @@ -29,48 +29,6 @@ const enumTokenHints = new Set([ EnumToken.StartMatchTokenType, EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType, EnumToken.DashMatchTokenType, EnumToken.ContainMatchTokenType, EnumToken.EOFTokenType ]); -const webkitPseudoAliasMap = { - '-webkit-autofill': 'autofill', - '-webkit-any': 'is', - '-moz-any': 'is', - '-webkit-border-after': 'border-block-end', - '-webkit-border-after-color': 'border-block-end-color', - '-webkit-border-after-style': 'border-block-end-style', - '-webkit-border-after-width': 'border-block-end-width', - '-webkit-border-before': 'border-block-start', - '-webkit-border-before-color': 'border-block-start-color', - '-webkit-border-before-style': 'border-block-start-style', - '-webkit-border-before-width': 'border-block-start-width', - '-webkit-border-end': 'border-inline-end', - '-webkit-border-end-color': 'border-inline-end-color', - '-webkit-border-end-style': 'border-inline-end-style', - '-webkit-border-end-width': 'border-inline-end-width', - '-webkit-border-start': 'border-inline-start', - '-webkit-border-start-color': 'border-inline-start-color', - '-webkit-border-start-style': 'border-inline-start-style', - '-webkit-border-start-width': 'border-inline-start-width', - '-webkit-box-align': 'align-items', - '-webkit-box-direction': 'flex-direction', - '-webkit-box-flex': 'flex-grow', - '-webkit-box-lines': 'flex-flow', - '-webkit-box-ordinal-group': 'order', - '-webkit-box-orient': 'flex-direction', - '-webkit-box-pack': 'justify-content', - '-webkit-column-break-after': 'break-after', - '-webkit-column-break-before': 'break-before', - '-webkit-column-break-inside': 'break-inside', - '-webkit-font-feature-settings': 'font-feature-settings', - '-webkit-hyphenate-character': 'hyphenate-character', - '-webkit-initial-letter': 'initial-letter', - '-webkit-margin-end': 'margin-block-end', - '-webkit-margin-start': 'margin-block-start', - '-webkit-padding-after': 'padding-block-end', - '-webkit-padding-before': 'padding-block-start', - '-webkit-padding-end': 'padding-inline-end', - '-webkit-padding-start': 'padding-inline-start', - '-webkit-min-device-pixel-ratio': 'min-resolution', - '-webkit-max-device-pixel-ratio': 'max-resolution' -}; function reject(reason) { throw new Error(reason ?? 'Parsing aborted'); } @@ -133,9 +91,10 @@ async function doParse(iterator, options = {}) { } const iter = tokenize(iterator); let item; + const rawTokens = []; while (item = iter.next().value) { stats.bytesIn = item.bytesIn; - // + rawTokens.push(item); // doParse error if (item.hint != null && BadTokensTypes.includes(item.hint)) { // bad token @@ -145,7 +104,8 @@ async function doParse(iterator, options = {}) { tokens.push(item); } if (item.token == ';' || item.token == '{') { - let node = await parseNode(tokens, context, stats, options, errors, src, map); + let node = await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (node != null) { // @ts-ignore stack.push(node); @@ -171,7 +131,8 @@ async function doParse(iterator, options = {}) { map = new Map; } else if (item.token == '}') { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; const previousNode = stack.pop(); // @ts-ignore context = stack[stack.length - 1] ?? ast; @@ -194,7 +155,8 @@ async function doParse(iterator, options = {}) { } } if (tokens.length > 0) { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (context != null && context.typ == EnumToken.InvalidRuleTokenType) { const index = context.chi.findIndex(node => node == context); if (index > -1) { @@ -301,7 +263,17 @@ async function doParse(iterator, options = {}) { } }; } -async function parseNode(results, context, stats, options, errors, src, map) { +function getLastNode(context) { + let i = context.chi.length; + while (i--) { + if ([EnumToken.CommentTokenType, EnumToken.CDOCOMMTokenType, EnumToken.WhitespaceTokenType].includes(context.chi[i].typ)) { + continue; + } + return context.chi[i]; + } + return null; +} +async function parseNode(results, context, stats, options, errors, src, map, rawTokens) { let tokens = []; for (const t of results) { const node = getTokenType(t.token, t.hint); @@ -356,24 +328,6 @@ async function parseNode(results, context, stats, options, errors, src, map) { if (tokens[0]?.typ == EnumToken.AtRuleTokenType) { const atRule = tokens.shift(); const position = map.get(atRule); - // if (atRule.val == 'charset') { - // - // if (context.typ != EnumToken.StyleSheetNodeType || context.chi.some(t => t.typ != EnumToken.CDOCOMMTokenType && t.typ != EnumToken.CommentNodeType)) { - // - // errors.push({ - // action: 'drop', - // message: 'doParse: invalid @charset', - // location: {src, ...position} - // }); - // - // return null; - // } - // - // if (options.removeCharset) { - // - // return null; - // } - // } // @ts-ignore while ([EnumToken.WhitespaceTokenType].includes(tokens[0]?.typ)) { tokens.shift(); @@ -470,8 +424,43 @@ async function parseNode(results, context, stats, options, errors, src, map) { // https://www.w3.org/TR/css-nesting-1/#conditionals // allowed nesting at-rules // there must be a top level rule in the stack - if (atRule.val == 'charset' && options.removeCharset) { - return null; + if (atRule.val == 'charset') { + let spaces = 0; + // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset + for (let k = 1; k < rawTokens.length; k++) { + if (rawTokens[k].hint == EnumToken.WhitespaceTokenType) { + spaces += rawTokens[k].len; + continue; + } + if (rawTokens[k].hint == EnumToken.CommentTokenType) { + continue; + } + if (rawTokens[k].hint == EnumToken.CDOCOMMTokenType) { + continue; + } + if (spaces > 1) { + errors.push({ + action: 'drop', + message: '@charset must have only one space', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + if (rawTokens[k].hint != EnumToken.StringTokenType || rawTokens[k].token[0] != '"') { + errors.push({ + action: 'drop', + message: '@charset expects a ""', + // @ts-ignore + location: { src, ...(map.get(atRule) ?? position) } + }); + return null; + } + break; + } + if (options.removeCharset) { + return null; + } } const t = parseAtRulePrelude(parseTokens(tokens, { minify: options.minify }), atRule); const raw = t.reduce((acc, curr) => { @@ -499,11 +488,39 @@ async function parseNode(results, context, stats, options, errors, src, map) { node.loc = loc; } if (options.validation) { - const valid = validateAtRule(node, options, context); + let isValid = true; + if (node.nam == 'else') { + const prev = getLastNode(context); + if (prev != null && prev.typ == EnumToken.AtRuleNodeType && ['when', 'else'].includes(prev.nam)) { + if (prev.nam == 'else') { + isValid = Array.isArray(prev.tokens) && prev.tokens.length > 0; + } + } + else { + isValid = false; + } + } + const valid = isValid ? validateAtRule(node, options, context) : { + valid: ValidationLevel.Drop, + node, + matches: [], + syntax: '@' + node.nam, + error: '@' + node.nam + ' not allowed here', + tokens + }; if (valid.valid == ValidationLevel.Drop) { + errors.push({ + action: 'drop', + message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), '') + '"', + // @ts-ignore + location: { src, ...(map.get(valid.node) ?? position) } + }); // @ts-ignore node.typ = EnumToken.InvalidAtRuleTokenType; } + else { + node.val = node.tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false, removeComments: true }), ''); + } } // @ts-ignore context.chi.push(node); @@ -759,17 +776,18 @@ function parseAtRulePrelude(tokens, atRule) { continue; } } - if (value.typ == EnumToken.ParensTokenType) { + if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == EnumToken.IdenTokenType) { + if ((dashedIdent && value.chi[i].typ == EnumToken.DashedIdenTokenType) || value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { nameIndex = i; } break; @@ -798,6 +816,13 @@ function parseAtRulePrelude(tokens, atRule) { ].includes(value.chi[valueIndex].typ)) { const val = value.chi.splice(valueIndex, 1)[0]; const node = value.chi.splice(nameIndex, 1)[0]; + // 'background' + // @ts-ignore + if (node.typ == EnumToken.ColorTokenType && node.kin == 'dpsys') { + // @ts-ignore + delete node.kin; + node.typ = EnumToken.IdenTokenType; + } while (value.chi[0]?.typ == EnumToken.WhitespaceTokenType) { value.chi.shift(); } diff --git a/dist/lib/parser/tokenize.js b/dist/lib/parser/tokenize.js index f9f49b6f..9fa6c251 100644 --- a/dist/lib/parser/tokenize.js +++ b/dist/lib/parser/tokenize.js @@ -16,7 +16,7 @@ function consumeWhiteSpace(parseInfo) { return count; } function pushToken(token, parseInfo, hint) { - const result = { token, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; + const result = { token, len: parseInfo.currentPosition.ind - parseInfo.position.ind, hint, position: { ...parseInfo.position }, bytesIn: parseInfo.currentPosition.ind + 1 }; parseInfo.position.ind = parseInfo.currentPosition.ind; parseInfo.position.lin = parseInfo.currentPosition.lin; parseInfo.position.col = Math.max(parseInfo.currentPosition.col, 1); @@ -184,8 +184,10 @@ function* tokenize(stream) { buffer += value; } } - yield pushToken(buffer, parseInfo, EnumToken.BadCommentTokenType); - buffer = ''; + if (buffer.length > 0) { + yield pushToken(buffer, parseInfo, EnumToken.BadCommentTokenType); + buffer = ''; + } } break; case '&': diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 59802d84..45edd1e9 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -42,16 +42,21 @@ function update(position, str) { } } function doRender(data, options = {}) { + const minify = options.minify ?? true; + const beautify = options.beautify ?? !minify; options = { - ...(options.minify ?? true ? { + ...(beautify ? { + indent: ' ', + newLine: '\n', + } : { indent: '', newLine: '', + }), + ...(minify ? { removeEmpty: true, removeComments: true } : { - indent: ' ', - newLine: '\n', - compress: false, + removeEmpty: false, removeComments: false, }), sourcemap: false, convertColor: true, expandNestingRules: false, preserveLicense: false, ...options }; diff --git a/dist/lib/syntax/syntax.js b/dist/lib/syntax/syntax.js index 165d0644..0e813550 100644 --- a/dist/lib/syntax/syntax.js +++ b/dist/lib/syntax/syntax.js @@ -25,6 +25,341 @@ const mediaTypes = ['all', 'print', 'screen', 'aural', 'braille', 'embossed', 'handheld', 'projection', 'tty', 'tv', 'speech']; // https://www.w3.org/TR/css-values-4/#math-function const mathFuncs = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; +const webkitPseudoAliasMap = { + '-webkit-autofill': 'autofill', + '-webkit-any': 'is', + '-moz-any': 'is', + '-webkit-border-after': 'border-block-end', + '-webkit-border-after-color': 'border-block-end-color', + '-webkit-border-after-style': 'border-block-end-style', + '-webkit-border-after-width': 'border-block-end-width', + '-webkit-border-before': 'border-block-start', + '-webkit-border-before-color': 'border-block-start-color', + '-webkit-border-before-style': 'border-block-start-style', + '-webkit-border-before-width': 'border-block-start-width', + '-webkit-border-end': 'border-inline-end', + '-webkit-border-end-color': 'border-inline-end-color', + '-webkit-border-end-style': 'border-inline-end-style', + '-webkit-border-end-width': 'border-inline-end-width', + '-webkit-border-start': 'border-inline-start', + '-webkit-border-start-color': 'border-inline-start-color', + '-webkit-border-start-style': 'border-inline-start-style', + '-webkit-border-start-width': 'border-inline-start-width', + '-webkit-box-align': 'align-items', + '-webkit-box-direction': 'flex-direction', + '-webkit-box-flex': 'flex-grow', + '-webkit-box-lines': 'flex-flow', + '-webkit-box-ordinal-group': 'order', + '-webkit-box-orient': 'flex-direction', + '-webkit-box-pack': 'justify-content', + '-webkit-column-break-after': 'break-after', + '-webkit-column-break-before': 'break-before', + '-webkit-column-break-inside': 'break-inside', + '-webkit-font-feature-settings': 'font-feature-settings', + '-webkit-hyphenate-character': 'hyphenate-character', + '-webkit-initial-letter': 'initial-letter', + '-webkit-margin-end': 'margin-block-end', + '-webkit-margin-start': 'margin-block-start', + '-webkit-padding-after': 'padding-block-end', + '-webkit-padding-before': 'padding-block-start', + '-webkit-padding-end': 'padding-inline-end', + '-webkit-padding-start': 'padding-inline-start', + '-webkit-min-device-pixel-ratio': 'min-resolution', + '-webkit-max-device-pixel-ratio': 'max-resolution' +}; +// https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions +// https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar +const webkitExtensions = new Set([ + '-webkit-app-region', + '-webkit-border-horizontal-spacing', + '-webkit-border-vertical-spacing', + '-webkit-box-reflect', + '-webkit-column-axis', + '-webkit-column-progression', + '-webkit-cursor-visibility', + '-webkit-font-smoothing', + '-webkit-hyphenate-limit-after', + '-webkit-hyphenate-limit-before', + '-webkit-hyphenate-limit-lines', + '-webkit-line-align', + '-webkit-line-box-contain', + '-webkit-line-clamp', + '-webkit-line-grid', + '-webkit-line-snap', + '-webkit-locale', + '-webkit-logical-height', + '-webkit-logical-width', + '-webkit-margin-after', + '-webkit-margin-before', + '-webkit-mask-box-image-outset', + '-webkit-mask-box-image-repeat', + '-webkit-mask-box-image-slice', + '-webkit-mask-box-image-source', + '-webkit-mask-box-image-width', + '-webkit-mask-box-image', + '-webkit-mask-composite', + '-webkit-mask-position-x', + '-webkit-mask-position-y', + '-webkit-mask-repeat-x', + '-webkit-mask-repeat-y', + '-webkit-mask-source-type', + '-webkit-max-logical-height', + '-webkit-max-logical-width', + '-webkit-min-logical-height', + '-webkit-min-logical-width', + '-webkit-nbsp-mode', + '-webkit-perspective-origin-x', + '-webkit-perspective-origin-y', + '-webkit-rtl-ordering', + '-webkit-tap-highlight-color', + '-webkit-text-decoration-skip', + '-webkit-text-decorations-in-effect', + '-webkit-text-fill-color', + '-webkit-text-security', + '-webkit-text-stroke-color', + '-webkit-text-stroke-width', + '-webkit-text-stroke', + '-webkit-text-zoom', + '-webkit-touch-callout', + '-webkit-transform-origin-x', + '-webkit-transform-origin-y', + '-webkit-transform-origin-z', + '-webkit-user-drag', + '-webkit-user-modify', + '-webkit-border-after', + '-webkit-border-after-color', + '-webkit-border-after-style', + '-webkit-border-after-width', + '-webkit-border-before', + '-webkit-border-before-color', + '-webkit-border-before-style', + '-webkit-border-before-width', + '-webkit-border-end', + '-webkit-border-end-color', + '-webkit-border-end-style', + '-webkit-border-end-width', + '-webkit-border-start', + '-webkit-border-start-color', + '-webkit-border-start-style', + '-webkit-border-start-width', + '-webkit-box-align', + '-webkit-box-direction', + '-webkit-box-flex-group', + '-webkit-box-flex', + '-webkit-box-lines', + '-webkit-box-ordinal-group', + '-webkit-box-orient', + '-webkit-box-pack', + '-webkit-column-break-after', + '-webkit-column-break-before', + '-webkit-column-break-inside', + '-webkit-font-feature-settings', + '-webkit-hyphenate-character', + '-webkit-initial-letter', + '-webkit-margin-end', + '-webkit-margin-start', + '-webkit-padding-after', + '-webkit-padding-before', + '-webkit-padding-end', + '-webkit-padding-start', + '-webkit-fill-available', + ':-webkit-animating-full-screen-transition', + ':-webkit-any', + ':-webkit-any-link', + ':-webkit-autofill', + ':-webkit-autofill-strong-password', + ':-webkit-drag', + ':-webkit-full-page-media', + ':-webkit-full-screen*', + ':-webkit-full-screen-ancestor', + ':-webkit-full-screen-document', + ':-webkit-full-screen-controls-hidden', + '::-webkit-file-upload-button*', + '::-webkit-inner-spin-button', + '::-webkit-input-placeholder', + '::-webkit-meter-bar', + '::-webkit-meter-even-less-good-value', + '::-webkit-meter-inner-element', + '::-webkit-meter-optimum-value', + '::-webkit-meter-suboptimum-value', + '::-webkit-progress-bar', + '::-webkit-progress-inner-element', + '::-webkit-progress-value', + '::-webkit-search-cancel-button', + '::-webkit-search-results-button', + '::-webkit-slider-runnable-track', + '::-webkit-slider-thumb', + '-webkit-animation', + '-webkit-device-pixel-ratio', + '-webkit-transform-2d', + '-webkit-transform-3d', + '-webkit-transition', + '::-webkit-scrollbar', + '::-webkit-scrollbar-button', + '::-webkit-scrollbar', + '::-webkit-scrollbar-thumb', + '::-webkit-scrollbar-track', + '::-webkit-scrollbar-track-piece', + '::-webkit-scrollbar:vertical', + '::-webkit-scrollbar-corner ', + '::-webkit-resizer', + ':vertical', + ':horizontal', +]); +// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions +const mozExtensions = new Set([ + '-moz-box-align', + '-moz-box-direction', + '-moz-box-flex', + '-moz-box-ordinal-group', + '-moz-box-orient', + '-moz-box-pack', + '-moz-float-edge', + '-moz-force-broken-image-icon', + '-moz-image-region', + '-moz-orient', + '-moz-osx-font-smoothing', + '-moz-user-focus', + '-moz-user-input', + '-moz-user-modify', + '-moz-animation', + '-moz-animation-delay', + '-moz-animation-direction', + '-moz-animation-duration', + '-moz-animation-fill-mode', + '-moz-animation-iteration-count', + '-moz-animation-name', + '-moz-animation-play-state', + '-moz-animation-timing-function', + '-moz-appearance', + '-moz-backface-visibility', + '-moz-background-clip', + '-moz-background-origin', + '-moz-background-inline-policy', + '-moz-background-size', + '-moz-border-end', + '-moz-border-end-color', + '-moz-border-end-style', + '-moz-border-end-width', + '-moz-border-image', + '-moz-border-start', + '-moz-border-start-color', + '-moz-border-start-style', + '-moz-border-start-width', + '-moz-box-sizing', + 'clip-path', + '-moz-column-count', + '-moz-column-fill', + '-moz-column-gap', + '-moz-column-width', + '-moz-column-rule', + '-moz-column-rule-width', + '-moz-column-rule-style', + '-moz-column-rule-color', + 'filter', + '-moz-font-feature-settings', + '-moz-font-language-override', + '-moz-hyphens', + '-moz-margin-end', + '-moz-margin-start', + 'mask', + '-moz-opacity', + '-moz-outline', + '-moz-outline-color', + '-moz-outline-offset', + '-moz-outline-style', + '-moz-outline-width', + '-moz-padding-end', + '-moz-padding-start', + '-moz-perspective', + '-moz-perspective-origin', + 'pointer-events', + '-moz-tab-size', + '-moz-text-align-last', + '-moz-text-decoration-color', + '-moz-text-decoration-line', + '-moz-text-decoration-style', + '-moz-text-size-adjust', + '-moz-transform', + '-moz-transform-origin', + '-moz-transform-style', + '-moz-transition', + '-moz-transition-delay', + '-moz-transition-duration', + '-moz-transition-property', + '-moz-transition-timing-function', + '-moz-user-select', + '-moz-initial', + '-moz-appearance', + '-moz-linear-gradient', + '-moz-radial-gradient', + '-moz-element', + '-moz-image-rect', + '::-moz-anonymous-block', + '::-moz-anonymous-positioned-block', + ':-moz-any', + ':-moz-any-link', + ':-moz-broken', + '::-moz-canvas', + '::-moz-color-swatch', + '::-moz-cell-content', + ':-moz-drag-over', + ':-moz-first-node', + '::-moz-focus-inner', + '::-moz-focus-outer', + ':-moz-full-screen', + ':-moz-full-screen-ancestor', + ':-moz-handler-blocked', + ':-moz-handler-crashed', + ':-moz-handler-disabled', + '::-moz-inline-table', + ':-moz-last-node', + '::-moz-list-bullet', + '::-moz-list-number', + ':-moz-loading', + ':-moz-locale-dir', + ':-moz-locale-dir', + ':-moz-lwtheme', + ':-moz-lwtheme-brighttext', + ':-moz-lwtheme-darktext', + '::-moz-meter-bar', + ':-moz-native-anonymous', + ':-moz-only-whitespace', + '::-moz-pagebreak', + '::-moz-pagecontent', + ':-moz-placeholder', + '::-moz-placeholder', + '::-moz-progress-bar', + '::-moz-range-progress', + '::-moz-range-thumb', + '::-moz-range-track', + ':-moz-read-only', + ':-moz-read-write', + '::-moz-scrolled-canvas', + '::-moz-scrolled-content', + '::-moz-selection', + ':-moz-submit-invalid', + ':-moz-suppressed', + '::-moz-svg-foreign-content', + '::-moz-table', + '::-moz-table-cell', + '::-moz-table-column', + '::-moz-table-column-group', + '::-moz-table-outer', + '::-moz-table-row', + '::-moz-table-row-group', + ':-moz-ui-invalid', + ':-moz-ui-valid', + ':-moz-user-disabled', + '::-moz-viewport', + '::-moz-viewport-scroll', + ':-moz-window-inactive', + '-moz-device-pixel-ratio', + '-moz-os-version', + '-moz-touch-enabled', + '-moz-windows-glass', + '-moz-alt-content' +]); function isLength(dimension) { return 'unit' in dimension && dimensionUnits.has(dimension.unit.toLowerCase()); } @@ -476,4 +811,4 @@ function isWhiteSpace(codepoint) { codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; } -export { colorFontTech, fontFeaturesTech, fontFormat, isAngle, isAtKeyword, isColor, isColorspace, isDigit, isDimension, isFlex, isFrequency, isFunction, isHash, isHexColor, isHueInterpolationMethod, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNonPrintable, isNumber, isPercentage, isPolarColorspace, isPseudo, isRectangularOrthogonalColorspace, isResolution, isTime, isWhiteSpace, mathFuncs, mediaTypes, parseDimension }; +export { colorFontTech, fontFeaturesTech, fontFormat, isAngle, isAtKeyword, isColor, isColorspace, isDigit, isDimension, isFlex, isFrequency, isFunction, isHash, isHexColor, isHueInterpolationMethod, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNonPrintable, isNumber, isPercentage, isPolarColorspace, isPseudo, isRectangularOrthogonalColorspace, isResolution, isTime, isWhiteSpace, mathFuncs, mediaTypes, mozExtensions, parseDimension, webkitExtensions, webkitPseudoAliasMap }; diff --git a/dist/lib/validation/at-rules/container.js b/dist/lib/validation/at-rules/container.js new file mode 100644 index 00000000..48d06d97 --- /dev/null +++ b/dist/lib/validation/at-rules/container.js @@ -0,0 +1,353 @@ +import { ValidationLevel, EnumToken } from '../../ast/types.js'; +import '../../ast/minify.js'; +import '../../ast/walk.js'; +import '../../parser/parse.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import '../../parser/utils/config.js'; +import { consumeWhitespace } from '../utils/whitespace.js'; +import { splitTokenList } from '../utils/list.js'; + +const validateContainerScrollStateFeature = validateContainerSizeFeature; +function validateAtRuleContainer(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + }; + } + const result = validateAtRuleContainerQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + }; +} +function validateAtRuleContainerQueryList(tokens, atRule) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + let result = null; + let tokenType = null; + for (const queries of splitTokenList(tokens)) { + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + result = null; + const match = []; + let token = null; + tokenType = null; + while (queries.length > 0) { + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + if (queries[0].typ == EnumToken.IdenTokenType) { + match.push(queries.shift()); + consumeWhitespace(queries); + } + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (token.typ != EnumToken.ParensTokenType && (token.typ != EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + if (token.typ == EnumToken.ParensTokenType) { + result = validateContainerSizeFeature(token.chi, atRule); + } + else if (token.val == 'scroll-state') { + result = validateContainerScrollStateFeature(token.chi, atRule); + } + else { + result = validateContainerStyleFeature(token.chi, atRule); + } + if (result.valid == ValidationLevel.Drop) { + return result; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ != EnumToken.MediaFeatureAndTokenType && token.typ != EnumToken.MediaFeatureOrTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = token.typ; + } + if (tokenType != token.typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; +} +function validateContainerStyleFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 1) { + if (tokens[0].typ == EnumToken.ParensTokenType) { + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + if ([EnumToken.DashedIdenTokenType, EnumToken.IdenTokenType].includes(tokens[0].typ) || + (tokens[0].typ == EnumToken.MediaQueryConditionTokenType && tokens[0].op.typ == EnumToken.ColonTokenType)) { + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + } + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; +} +function validateContainerSizeFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + if (tokens.length == 1) { + const token = tokens[0]; + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + return validateContainerSizeFeature([token.val], atRule); + } + if (token.typ == EnumToken.ParensTokenType) { + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); +} +function validateAtRuleContainerQueryStyleInParams(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + let token = tokens[0]; + let tokenType = null; + let result = null; + while (tokens.length > 0) { + token = tokens[0]; + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (tokens[0].typ != EnumToken.ParensTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + const slices = tokens[0].chi.slice(); + consumeWhitespace(slices); + if (slices.length == 1) { + if ([EnumToken.MediaFeatureNotTokenType, EnumToken.ParensTokenType].includes(slices[0].typ)) { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + else if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + } + else { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + break; + } + if (![EnumToken.MediaFeatureAndTokenType, EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = tokens[0].typ; + } + if (tokenType != tokens[0].typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; +} + +export { validateAtRuleContainer }; diff --git a/dist/lib/validation/at-rules/counter-style.js b/dist/lib/validation/at-rules/counter-style.js index e5cd6e0f..a51c92f4 100644 --- a/dist/lib/validation/at-rules/counter-style.js +++ b/dist/lib/validation/at-rules/counter-style.js @@ -15,7 +15,7 @@ function validateAtRuleCounterStyle(atRule, options, root) { matches: [], node: atRule, syntax: '@counter-style', - error: 'expected media query list', + error: 'expected counter style name', tokens: [] }; } @@ -23,7 +23,7 @@ function validateAtRuleCounterStyle(atRule, options, root) { if (tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Valid, + valid: ValidationLevel.Drop, matches: [], node: atRule, syntax: '@counter-style', diff --git a/dist/lib/validation/at-rules/custom-media.js b/dist/lib/validation/at-rules/custom-media.js new file mode 100644 index 00000000..48f61b54 --- /dev/null +++ b/dist/lib/validation/at-rules/custom-media.js @@ -0,0 +1,52 @@ +import { ValidationLevel, EnumToken } from '../../ast/types.js'; +import '../../ast/minify.js'; +import '../../ast/walk.js'; +import '../../parser/parse.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import '../../parser/utils/config.js'; +import { consumeWhitespace } from '../utils/whitespace.js'; +import { validateAtRuleMediaQueryList } from './media.js'; + +function validateAtRuleCustomMedia(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + }; + } + const queries = atRule.tokens.slice(); + consumeWhitespace(queries); + if (queries.length == 0 || queries[0].typ != EnumToken.DashedIdenTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@custom-media', + error: 'expecting dashed identifier', + tokens: [] + }; + } + queries.shift(); + const result = validateAtRuleMediaQueryList(queries, atRule); + if (result.valid == ValidationLevel.Drop) { + atRule.tokens = []; + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@custom-media', + error: '', + tokens: [] + }; + } + return result; +} + +export { validateAtRuleCustomMedia }; diff --git a/dist/lib/validation/at-rules/else.js b/dist/lib/validation/at-rules/else.js new file mode 100644 index 00000000..a087e958 --- /dev/null +++ b/dist/lib/validation/at-rules/else.js @@ -0,0 +1,5 @@ +import { validateAtRuleWhen } from './when.js'; + +const validateAtRuleElse = validateAtRuleWhen; + +export { validateAtRuleElse }; diff --git a/dist/lib/validation/at-rules/media.js b/dist/lib/validation/at-rules/media.js index 657e6217..8f7de66f 100644 --- a/dist/lib/validation/at-rules/media.js +++ b/dist/lib/validation/at-rules/media.js @@ -13,15 +13,28 @@ function validateAtRuleMedia(atRule, options, root) { if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + }; + } + let result = null; + const slice = atRule.tokens.slice(); + consumeWhitespace(slice); + if (slice.length == 0) { + return { + valid: ValidationLevel.Valid, matches: [], node: atRule, syntax: '@media', - error: 'expected media query list', + error: '', tokens: [] }; } - const result = validateAtRuleMediaQueryList(atRule.tokens, atRule); + result = validateAtRuleMediaQueryList(atRule.tokens, atRule); if (result.valid == ValidationLevel.Drop) { return result; } @@ -47,10 +60,20 @@ function validateAtRuleMedia(atRule, options, root) { }; } function validateAtRuleMediaQueryList(tokenList, atRule) { - for (const tokens of splitTokenList(tokenList)) { + const split = splitTokenList(tokenList); + const matched = []; + let result = null; + let previousToken; + let mediaFeatureType; + for (let i = 0; i < split.length; i++) { + const tokens = split[i].slice(); + const match = []; + result = null; + mediaFeatureType = null; + previousToken = null; if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -58,26 +81,38 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'unexpected token', tokens: [] }; + continue; } - let previousToken = null; while (tokens.length > 0) { - // media-condition - if (validateMediaCondition(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); - } - // media-type - else if (validateMediaFeature(tokens[0])) { - previousToken = tokens[0]; - tokens.shift(); + previousToken = tokens[0]; + // media-condition | media-type | custom-media + if (!(validateMediaCondition(tokens[0], atRule) || validateMediaFeature(tokens[0]) || validateCustomMediaCondition(tokens[0], atRule))) { + if (tokens[0].typ == EnumToken.ParensTokenType) { + result = validateAtRuleMediaQueryList(tokens[0].chi, atRule); + } + else { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0] ?? atRule, + syntax: '@media', + error: 'expecting media feature or media condition', + tokens: [] + }; + } + if (result.valid == ValidationLevel.Drop) { + break; + } + result = null; } + match.push(tokens.shift()); if (tokens.length == 0) { break; } if (!consumeWhitespace(tokens)) { if (previousToken?.typ != EnumToken.ParensTokenType) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -85,11 +120,12 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'expected media query list', tokens: [] }; + break; } } - if (![EnumToken.MediaFeatureOrTokenType, EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { + else if (![EnumToken.MediaFeatureOrTokenType, EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -97,31 +133,70 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { error: 'expected and/or', tokens: [] }; + break; + } + if (mediaFeatureType == null) { + mediaFeatureType = tokens[0]; } - if (tokens.length == 1) { + if (mediaFeatureType.typ != tokens[0].typ) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected media-condition', + error: 'mixing and/or not allowed at the same level', tokens: [] }; + break; } - tokens.shift(); - if (!consumeWhitespace(tokens)) { + match.push({ typ: EnumToken.WhitespaceTokenType }, tokens.shift()); + consumeWhitespace(tokens); + if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected whitespace', + error: 'expected media-condition', tokens: [] }; + break; } + match.push({ typ: EnumToken.WhitespaceTokenType }); } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@media', + error: 'expected media query list', + tokens: [] + }; + } + tokenList.length = 0; + let hasAll = false; + for (let i = 0; i < matched.length; i++) { + if (tokenList.length > 0) { + tokenList.push({ typ: EnumToken.CommaTokenType }); + } + if (matched[i].length == 1 && matched.length > 1 && matched[i][0].typ == EnumToken.MediaFeatureTokenType && matched[i][0].val == 'all') { + hasAll = true; + continue; + } + tokenList.push(...matched[i]); + } + if (hasAll && tokenList.length == 0) { + tokenList.push({ typ: EnumToken.MediaFeatureTokenType, val: 'all' }); } // @ts-ignore return { @@ -133,9 +208,9 @@ function validateAtRuleMediaQueryList(tokenList, atRule) { tokens: [] }; } -function validateMediaCondition(token) { +function validateCustomMediaCondition(token, atRule) { if (token.typ == EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(token.val); + return validateMediaCondition(token.val, atRule); } if (token.typ != EnumToken.ParensTokenType) { return false; @@ -144,11 +219,24 @@ function validateMediaCondition(token) { if (chi.length != 1) { return false; } + return chi[0].typ == EnumToken.DashedIdenTokenType; +} +function validateMediaCondition(token, atRule) { + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + return validateMediaCondition(token.val, atRule); + } + if (token.typ != EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == EnumToken.FunctionTokenType && ['media', 'supports'].includes(token.val))) { + return false; + } + const chi = token.chi.filter((t) => t.typ != EnumToken.CommentTokenType && t.typ != EnumToken.WhitespaceTokenType); + if (chi.length != 1) { + return false; + } if (chi[0].typ == EnumToken.IdenTokenType) { return true; } if (chi[0].typ == EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(chi[0].val); + return validateMediaCondition(chi[0].val, atRule); } if (chi[0].typ == EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == EnumToken.IdenTokenType; @@ -163,4 +251,4 @@ function validateMediaFeature(token) { return val.typ == EnumToken.MediaFeatureTokenType; } -export { validateAtRuleMedia, validateAtRuleMediaQueryList }; +export { validateAtRuleMedia, validateAtRuleMediaQueryList, validateMediaCondition, validateMediaFeature }; diff --git a/dist/lib/validation/at-rules/supports.js b/dist/lib/validation/at-rules/supports.js index 2977d08f..f6797492 100644 --- a/dist/lib/validation/at-rules/supports.js +++ b/dist/lib/validation/at-rules/supports.js @@ -19,7 +19,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports query list', tokens: [] }; @@ -37,7 +37,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected at-rule body', tokens: [] }; @@ -47,7 +47,7 @@ function validateAtRuleSupports(atRule, options, root) { valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; @@ -60,7 +60,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'unexpected token', tokens: [] }; @@ -94,7 +94,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? previousToken ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -106,7 +106,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected and/or', tokens: [] }; @@ -117,7 +117,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports-condition', tokens: [] }; @@ -129,7 +129,7 @@ function validateAtRuleSupportsConditions(atRule, tokenList) { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] }; @@ -142,13 +142,13 @@ function validateSupportCondition(atRule, token) { if (token.typ == EnumToken.MediaFeatureNotTokenType) { return validateSupportCondition(atRule, token.val); } - if (token.typ != EnumToken.ParensTokenType) { + if (token.typ != EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == EnumToken.FunctionTokenType && ['supports', 'font-format', 'font-tech'].includes(token.val))) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], node: token, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports condition-in-parens', tokens: [] }; @@ -163,7 +163,7 @@ function validateSupportCondition(atRule, token) { valid: ValidationLevel.Valid, matches: [], node: null, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; diff --git a/dist/lib/validation/at-rules/when.js b/dist/lib/validation/at-rules/when.js new file mode 100644 index 00000000..7ed9ee6b --- /dev/null +++ b/dist/lib/validation/at-rules/when.js @@ -0,0 +1,178 @@ +import { ValidationLevel, EnumToken } from '../../ast/types.js'; +import '../../ast/minify.js'; +import '../../ast/walk.js'; +import '../../parser/parse.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import '../../parser/utils/config.js'; +import { consumeWhitespace } from '../utils/whitespace.js'; +import { splitTokenList } from '../utils/list.js'; +import { validateMediaFeature, validateMediaCondition } from './media.js'; +import { validateSupportCondition } from './supports.js'; + +function validateAtRuleWhen(atRule, options, root) { + const slice = Array.isArray(atRule.tokens) ? atRule.tokens.slice() : []; + consumeWhitespace(slice); + if (slice.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: [] + }; + } + const result = validateAtRuleWhenQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@when', + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: result.tokens + }; +} +// media() = media( [ | | ] ) +// supports() = supports( ) +function validateAtRuleWhenQueryList(tokenList, atRule) { + const matched = []; + let result = null; + for (const split of splitTokenList(tokenList)) { + const match = []; + result = null; + consumeWhitespace(split); + if (split.length == 0) { + continue; + } + while (split.length > 0) { + if (split[0].typ != EnumToken.FunctionTokenType || !['media', 'supports', 'font-tech', 'font-format'].includes(split[0].val)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'unexpected token', + tokens: [] + }; + break; + } + const chi = split[0].chi.slice(); + consumeWhitespace(chi); + if (split[0].val == 'media') { + // result = valida + if (chi.length != 1 || !(validateMediaFeature(chi[0]) || validateMediaCondition(split[0], atRule))) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + else if (['supports', 'font-tech', 'font-format'].includes(split[0].val)) { + // result = valida + if (!validateSupportCondition(atRule, split[0])) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + }; + break; + } + } + if (match.length > 0) { + match.push({ typ: EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + break; + } + if (![EnumToken.MediaFeatureAndTokenType, EnumToken.MediaFeatureOrTokenType].includes(split[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting and/or media-condition', + tokens: [] + }; + break; + } + if (match.length > 0) { + match.push({ typ: EnumToken.WhitespaceTokenType }); + } + match.push(split.shift()); + consumeWhitespace(split); + if (split.length == 0) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting media-condition', + tokens: [] + }; + break; + } + } + if (result == null && match.length > 0) { + matched.push(match); + } + } + if (result != null) { + return result; + } + if (matched.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: result?.node ?? atRule, + syntax: '@when', + error: 'invalid at-rule body', + tokens: [] + }; + } + tokenList.length = 0; + for (const match of matched) { + if (tokenList.length > 0) { + tokenList.push({ + typ: EnumToken.CommaTokenType + }); + } + tokenList.push(...match); + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: tokenList + }; +} + +export { validateAtRuleWhen, validateAtRuleWhenQueryList }; diff --git a/dist/lib/validation/atrule.js b/dist/lib/validation/atrule.js index 45c11e56..b0cc36df 100644 --- a/dist/lib/validation/atrule.js +++ b/dist/lib/validation/atrule.js @@ -17,6 +17,10 @@ import { validateAtRuleFontFeatureValues } from './at-rules/font-feature-values. import { validateAtRuleNamespace } from './at-rules/namespace.js'; import { validateAtRuleDocument } from './at-rules/document.js'; import { validateAtRuleKeyframes } from './at-rules/keyframes.js'; +import { validateAtRuleWhen } from './at-rules/when.js'; +import { validateAtRuleElse } from './at-rules/else.js'; +import { validateAtRuleContainer } from './at-rules/container.js'; +import { validateAtRuleCustomMedia } from './at-rules/custom-media.js'; function validateAtRule(atRule, options, root) { if (atRule.nam == 'charset') { @@ -60,9 +64,21 @@ function validateAtRule(atRule, options, root) { if (atRule.nam == 'namespace') { return validateAtRuleNamespace(atRule); } + if (atRule.nam == 'when') { + return validateAtRuleWhen(atRule); + } + if (atRule.nam == 'else') { + return validateAtRuleElse(atRule); + } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } + if (atRule.nam == 'custom-media') { + return validateAtRuleCustomMedia(atRule); + } if (['position-try', 'property', 'font-palette-values'].includes(atRule.nam)) { if (!('tokens' in atRule)) { return { diff --git a/dist/lib/validation/selector.js b/dist/lib/validation/selector.js index e6c6c7c2..94dbd3f2 100644 --- a/dist/lib/validation/selector.js +++ b/dist/lib/validation/selector.js @@ -12,7 +12,7 @@ import { validateSelectorList } from './syntaxes/selector-list.js'; function validateSelector(selector, options, root) { if (root == null) { - return validateRelativeSelectorList(selector, root); + return validateSelectorList(selector, root); } // @ts-ignore if (root.typ == EnumToken.AtRuleNodeType && root.nam.match(/^(-[a-z]+-)?keyframes$/)) { diff --git a/dist/lib/validation/syntaxes/complex-selector-list.js b/dist/lib/validation/syntaxes/complex-selector-list.js index eddc57ac..9d075f1e 100644 --- a/dist/lib/validation/syntaxes/complex-selector-list.js +++ b/dist/lib/validation/syntaxes/complex-selector-list.js @@ -1,4 +1,4 @@ -import { ValidationLevel, EnumToken } from '../../ast/types.js'; +import { ValidationLevel } from '../../ast/types.js'; import '../../ast/minify.js'; import '../../ast/walk.js'; import '../../parser/parse.js'; @@ -7,6 +7,7 @@ import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; import { validateSelector } from './selector.js'; import { consumeWhitespace } from '../utils/whitespace.js'; +import { splitTokenList } from '../utils/list.js'; function validateComplexSelectorList(tokens, root, options) { tokens = tokens.slice(); @@ -22,20 +23,23 @@ function validateComplexSelectorList(tokens, root, options) { tokens }; } - let i = -1; - let j = 0; let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == EnumToken.CommaTokenType) { - result = validateSelector(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + for (const t of splitTokenList(tokens)) { + result = validateSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expecting complex selector list', + tokens + }; } export { validateComplexSelectorList }; diff --git a/dist/lib/validation/syntaxes/complex-selector.js b/dist/lib/validation/syntaxes/complex-selector.js index 2070c099..96ab4de3 100644 --- a/dist/lib/validation/syntaxes/complex-selector.js +++ b/dist/lib/validation/syntaxes/complex-selector.js @@ -1,4 +1,5 @@ import { consumeWhitespace } from '../utils/whitespace.js'; +import { splitTokenList } from '../utils/list.js'; import { EnumToken, ValidationLevel } from '../../ast/types.js'; import '../../ast/minify.js'; import '../../ast/walk.js'; @@ -6,9 +7,11 @@ import '../../parser/parse.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; +import { validateCompoundSelector } from './compound-selector.js'; const combinatorsTokens = [EnumToken.ChildCombinatorTokenType, EnumToken.ColumnCombinatorTokenType, - EnumToken.DescendantCombinatorTokenType, EnumToken.NextSiblingCombinatorTokenType, EnumToken.SubsequentSiblingCombinatorTokenType]; + // EnumToken.DescendantCombinatorTokenType, + EnumToken.NextSiblingCombinatorTokenType, EnumToken.SubsequentSiblingCombinatorTokenType]; // [ ? ]* function validateComplexSelector(tokens, root, options) { // [ ? * [ * ]* ]! @@ -25,257 +28,20 @@ function validateComplexSelector(tokens, root, options) { tokens }; } - while (tokens.length > 0) { - if (combinatorsTokens.includes(tokens[0].typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'unexpected combinator', - tokens - }; - } - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - }; - } - while (tokens.length > 0 && tokens[0].typ == EnumToken.NestingSelectorTokenType) { - tokens.shift(); - consumeWhitespace(tokens); - } - if (tokens.length == 0) { - break; - } - } - if (EnumToken.IdenTokenType == tokens[0].typ) { - tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - break; - } - } - if (EnumToken.UniversalSelectorTokenType == tokens[0].typ) { - tokens.shift(); - consumeWhitespace(tokens); - } - while (tokens.length > 0) { - if (tokens[0].typ == EnumToken.PseudoClassFuncTokenType) { - if (tokens[0].val.startsWith(':-webkit-')) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'invalid pseudo-class', - tokens - }; - } - } - if ([ - EnumToken.ClassSelectorTokenType, - EnumToken.HashTokenType, - EnumToken.PseudoClassTokenType, - EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - // validate namespace - if (tokens[0].typ == EnumToken.NameSpaceAttributeTokenType) { - if (!((tokens[0].l == null || tokens[0].l.typ == EnumToken.IdenTokenType || (tokens[0].l.typ == EnumToken.LiteralTokenType && tokens[0].l.val == '*')) && - tokens[0].r.typ == EnumToken.IdenTokenType)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting wq-name', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - // validate attribute - else if (tokens[0].typ == EnumToken.AttrTokenType) { - const children = tokens[0].chi.slice(); - consumeWhitespace(children); - if (children.length == 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - if (![ - EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType, - EnumToken.MatchExpressionTokenType - ].includes(children[0].typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - if (children[0].typ == EnumToken.MatchExpressionTokenType) { - if (![EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType].includes(children[0].l.typ) || - ![ - EnumToken.EqualMatchTokenType, EnumToken.DashMatchTokenType, - EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, - EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType - ].includes(children[0].op.typ) || - ![EnumToken.StringTokenType, - EnumToken.IdenTokenType].includes(children[0].r.typ)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - }; - } - } - children.shift(); - consumeWhitespace(children); - if (children.length > 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: null, - error: 'unexpected token', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - break; - } - if (tokens.length == 0) { - break; - } - // combinator - if (!combinatorsTokens.includes(tokens[0].typ)) { - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - if (!options?.nestedSelector) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - }; - } - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - if (tokens.length > 0 && - [ - EnumToken.IdenTokenType, - EnumToken.AttrTokenType, - EnumToken.NameSpaceAttributeTokenType, - EnumToken.ClassSelectorTokenType, - EnumToken.HashTokenType, - EnumToken.PseudoClassTokenType, - EnumToken.PseudoClassFuncTokenType - ].includes(tokens[0].typ)) { - continue; - } - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting combinator or subclass-selector', - tokens - }; - } - const token = tokens.shift(); - consumeWhitespace(tokens); - if (tokens.length == 0) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax: null, - error: 'expected compound-selector', - tokens - }; + let result = null; + for (const t of splitTokenList(tokens, combinatorsTokens)) { + result = validateCompoundSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } // @ts-ignore - return { - valid: ValidationLevel.Valid, + return result ?? { + valid: ValidationLevel.Drop, matches: [], - node: null, + node: root, syntax: null, - error: '', + error: 'expecting compound-selector', tokens }; } diff --git a/dist/lib/validation/syntaxes/compound-selector.js b/dist/lib/validation/syntaxes/compound-selector.js new file mode 100644 index 00000000..8e15b53c --- /dev/null +++ b/dist/lib/validation/syntaxes/compound-selector.js @@ -0,0 +1,222 @@ +import { ValidationLevel, EnumToken } from '../../ast/types.js'; +import '../../ast/minify.js'; +import '../../ast/walk.js'; +import '../../parser/parse.js'; +import { mozExtensions, webkitExtensions } from '../../syntax/syntax.js'; +import '../../parser/utils/config.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import { consumeWhitespace } from '../utils/whitespace.js'; +import { getSyntaxConfig } from '../config.js'; + +function validateCompoundSelector(tokens, root, options) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected selector', + tokens + }; + } + tokens = tokens.slice(); + consumeWhitespace(tokens); + const config = getSyntaxConfig(); + let match = 0; + let length = tokens.length; + while (tokens.length > 0) { + while (tokens.length > 0 && tokens[0].typ == EnumToken.NestingSelectorTokenType) { + if (!options?.nestedSelector) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'nested selector not allowed', + tokens + }; + } + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + // + while (tokens.length > 0 && + [ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType, + EnumToken.ClassSelectorTokenType, + EnumToken.HashTokenType, + EnumToken.UniversalSelectorTokenType + ].includes(tokens[0].typ)) { + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == EnumToken.PseudoClassFuncTokenType) { + if (!mozExtensions.has(tokens[0].val + '()') && + !webkitExtensions.has(tokens[0].val + '()') && + !((tokens[0].val + '()') in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val + '()', + tokens + }; + } + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == EnumToken.PseudoClassTokenType) { + const isPseudoElement = tokens[0].val.startsWith('::'); + if ( + // https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions#pseudo-elements + !(isPseudoElement && tokens[0].val.startsWith('::-webkit-')) && + !mozExtensions.has(tokens[0].val) && + !webkitExtensions.has(tokens[0].val) && + !(tokens[0].val in config.selectors) && + !(!isPseudoElement && + (':' + tokens[0].val) in config.selectors)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val, + tokens + }; + } + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + while (tokens.length > 0 && tokens[0].typ == EnumToken.AttrTokenType) { + const children = tokens[0].chi.slice(); + consumeWhitespace(children); + if (children.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + if (![ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType, + EnumToken.MatchExpressionTokenType + ].includes(children[0].typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + if (children[0].typ == EnumToken.MatchExpressionTokenType) { + if (children.length != 1) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid ', + tokens + }; + } + if (![ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType + ].includes(children[0].l.typ) || + ![ + EnumToken.EqualMatchTokenType, EnumToken.DashMatchTokenType, + EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, + EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType + ].includes(children[0].op.typ) || + ![ + EnumToken.StringTokenType, + EnumToken.IdenTokenType + ].includes(children[0].r.typ)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + }; + } + } + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + if (length == tokens.length) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + // @ts-ignore + syntax: null, + error: 'expected compound selector', + tokens + }; + } + length = tokens.length; + } + return match == 0 ? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected compound selector', + tokens + } : + // @ts-ignore + { + valid: ValidationLevel.Valid, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: null, + tokens + }; +} + +export { validateCompoundSelector }; diff --git a/dist/lib/validation/syntaxes/relative-selector-list.js b/dist/lib/validation/syntaxes/relative-selector-list.js index f5c6f9bf..80a62ca6 100644 --- a/dist/lib/validation/syntaxes/relative-selector-list.js +++ b/dist/lib/validation/syntaxes/relative-selector-list.js @@ -1,4 +1,4 @@ -import { EnumToken, ValidationLevel } from '../../ast/types.js'; +import { ValidationLevel } from '../../ast/types.js'; import '../../ast/minify.js'; import '../../ast/walk.js'; import '../../parser/parse.js'; @@ -6,22 +6,52 @@ import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; import { validateRelativeSelector } from './relative-selector.js'; +import { consumeWhitespace } from '../utils/whitespace.js'; +import { splitTokenList } from '../utils/list.js'; function validateRelativeSelectorList(tokens, root, options) { - let i = 0; - let j = 0; - let result = null; - while (i + 1 < tokens.length) { - if (tokens[++i].typ == EnumToken.CommaTokenType) { - result = validateRelativeSelector(tokens.slice(j, i), root, options); - if (result.valid == ValidationLevel.Drop) { - return result; - } - j = i + 1; - i = j; + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expecting relative selector list', + tokens + }; + } + for (const t of splitTokenList(tokens)) { + if (t.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'unexpected comma', + tokens + }; + } + const result = validateRelativeSelector(t, root, options); + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateRelativeSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + return { + valid: ValidationLevel.Valid, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: '', + tokens + }; } export { validateRelativeSelectorList }; diff --git a/dist/lib/validation/utils/list.js b/dist/lib/validation/utils/list.js index f3c8a559..d4ac4017 100644 --- a/dist/lib/validation/utils/list.js +++ b/dist/lib/validation/utils/list.js @@ -6,12 +6,12 @@ import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; -function splitTokenList(tokenList) { +function splitTokenList(tokenList, split = [EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == EnumToken.CommentTokenType) { return acc; } - if (curr.typ == EnumToken.CommaTokenType) { + if (split.includes(curr.typ)) { acc.push([]); } else { diff --git a/jsr.json b/jsr.json index 1e9e8492..55df19af 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.8.0", + "version": "v0.9.0-alpha1", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 11b02983..901b2627 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "0.8.0", + "version": "v0.9.0-alpha1", "exports": { ".": "./dist/node/index.js", "./umd": "./dist/index-umd-web.js", diff --git a/src/@types/ast.d.ts b/src/@types/ast.d.ts index 4de59360..160e2eb1 100644 --- a/src/@types/ast.d.ts +++ b/src/@types/ast.d.ts @@ -69,6 +69,7 @@ export declare interface AstKeyFrameRule extends BaseToken { chi: Array; optimized?: OptimizedSelector; raw?: RawSelectorTokens; + tokens?: Token[] } export declare type RawSelectorTokens = string[][]; @@ -80,6 +81,13 @@ export declare interface OptimizedSelector { reducible: boolean; } +export declare interface OptimizedSelectorToken { + match: boolean; + optimized: Token[]; + selector: Token[][], + reducible: boolean; +} + export declare interface AstAtRule extends BaseToken { typ: EnumToken.AtRuleNodeType, diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 58f41fb9..2aed0faa 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -1,5 +1,5 @@ import {VisitorNodeMap} from "./visitor.d.ts"; -import {AstAtRule, AstRule, AstRuleStyleSheet, Position, AstDeclaration} from "./ast.d.ts"; +import {AstAtRule, AstDeclaration, AstRule, AstRuleStyleSheet, Position} from "./ast.d.ts"; import {SourceMap} from "../lib/renderer/sourcemap"; import {PropertyListOptions} from "./parse.d.ts"; import {EnumToken} from "../lib"; @@ -94,6 +94,7 @@ export declare interface ResolvedPath { export declare interface RenderOptions { minify?: boolean; + beautify?: boolean; removeEmpty?: boolean; expandNestingRules?: boolean; preserveLicense?: boolean; @@ -107,7 +108,6 @@ export declare interface RenderOptions { cwd?: string; load?: (url: string, currentUrl: string) => Promise; resolve?: (url: string, currentUrl: string, currentWorkingDirectory?: string) => ResolvedPath; - } export declare interface TransformOptions extends ParserOptions, RenderOptions { @@ -151,6 +151,7 @@ export declare interface ParseTokenOptions extends ParserOptions { export declare interface TokenizeResult { token: string; + len: number; hint?: EnumToken; position: Position; bytesIn: number; diff --git a/src/@types/token.d.ts b/src/@types/token.d.ts index 0a3cc4a3..4a668fd7 100644 --- a/src/@types/token.d.ts +++ b/src/@types/token.d.ts @@ -449,12 +449,6 @@ export declare interface MediaFeatureOnlyToken extends BaseToken { val: Token; } -export declare interface MediaFeatureNotToken extends BaseToken { - - typ: EnumToken.MediaFeatureNotTokenType, - val: Token; -} - export declare interface MediaFeatureAndToken extends BaseToken { typ: EnumToken.MediaFeatureAndTokenType; diff --git a/src/lib/ast/minify.ts b/src/lib/ast/minify.ts index 40e29422..1fbbfc41 100644 --- a/src/lib/ast/minify.ts +++ b/src/lib/ast/minify.ts @@ -89,6 +89,7 @@ export function minify(ast: AstNode, options: ParserOptions | MinifyOptions = {} return acc; } + // @ts-ignore if ('chi' in ast && ast.chi.length > 0) { @@ -127,7 +128,8 @@ export function minify(ast: AstNode, options: ParserOptions | MinifyOptions = {} if (node.typ == EnumToken.AtRuleNodeType) { - if ((node).nam == 'media' && (node).val == 'all') { + // @ts-ignore + if ((node).nam == 'media' && ['all', '', null].includes((node).val)) { // @ts-ignore ast.chi?.splice(i, 1, ...node.chi); @@ -165,6 +167,7 @@ export function minify(ast: AstNode, options: ParserOptions | MinifyOptions = {} if (node.typ == EnumToken.RuleNodeType) { reduceRuleSelector(node); + let wrapper: AstRule; let match; @@ -540,7 +543,24 @@ export function minify(ast: AstNode, options: ParserOptions | MinifyOptions = {} return ast; } -export function reduceSelector(selector: string[][]) { +export function hasDeclaration(node: AstRule): boolean { + + // @ts-ignore + for (let i = 0; i < node.chi?.length; i++) { + + // @ts-ignore + if (node.chi[i].typ == EnumToken.CommentNodeType) { + + continue; + } + // @ts-ignore + return node.chi[i].typ == EnumToken.DeclarationNodeType; + } + + return true; +} + +export function reduceSelector(selector: string[][]): OptimizedSelector | null { if (selector.length == 0) { return null; @@ -679,23 +699,6 @@ export function reduceSelector(selector: string[][]) { }; } -export function hasDeclaration(node: AstRule): boolean { - - // @ts-ignore - for (let i = 0; i < node.chi?.length; i++) { - - // @ts-ignore - if (node.chi[i].typ == EnumToken.CommentNodeType) { - - continue; - } - // @ts-ignore - return node.chi[i].typ == EnumToken.DeclarationNodeType; - } - - return true; -} - export function splitRule(buffer: string): string[][] { const result: string[][] = [[]]; diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index bca3b2eb..bde78f4c 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -13,7 +13,8 @@ import { isPseudo, mathFuncs, mediaTypes, - parseDimension + parseDimension, + webkitPseudoAliasMap } from "../syntax"; import {parseDeclarationNode} from './utils'; import {renderToken} from "../renderer"; @@ -120,49 +121,6 @@ const enumTokenHints: Set = new Set([ EnumToken.EOFTokenType ]); -const webkitPseudoAliasMap: Record = { - '-webkit-autofill': 'autofill', - '-webkit-any': 'is', - '-moz-any': 'is', - '-webkit-border-after': 'border-block-end', - '-webkit-border-after-color': 'border-block-end-color', - '-webkit-border-after-style': 'border-block-end-style', - '-webkit-border-after-width': 'border-block-end-width', - '-webkit-border-before': 'border-block-start', - '-webkit-border-before-color': 'border-block-start-color', - '-webkit-border-before-style': 'border-block-start-style', - '-webkit-border-before-width': 'border-block-start-width', - '-webkit-border-end': 'border-inline-end', - '-webkit-border-end-color': 'border-inline-end-color', - '-webkit-border-end-style': 'border-inline-end-style', - '-webkit-border-end-width': 'border-inline-end-width', - '-webkit-border-start': 'border-inline-start', - '-webkit-border-start-color': 'border-inline-start-color', - '-webkit-border-start-style': 'border-inline-start-style', - '-webkit-border-start-width': 'border-inline-start-width', - '-webkit-box-align': 'align-items', - '-webkit-box-direction': 'flex-direction', - '-webkit-box-flex': 'flex-grow', - '-webkit-box-lines': 'flex-flow', - '-webkit-box-ordinal-group': 'order', - '-webkit-box-orient': 'flex-direction', - '-webkit-box-pack': 'justify-content', - '-webkit-column-break-after': 'break-after', - '-webkit-column-break-before': 'break-before', - '-webkit-column-break-inside': 'break-inside', - '-webkit-font-feature-settings': 'font-feature-settings', - '-webkit-hyphenate-character': 'hyphenate-character', - '-webkit-initial-letter': 'initial-letter', - '-webkit-margin-end': 'margin-block-end', - '-webkit-margin-start': 'margin-block-start', - '-webkit-padding-after': 'padding-block-end', - '-webkit-padding-before': 'padding-block-start', - '-webkit-padding-end': 'padding-inline-end', - '-webkit-padding-start': 'padding-inline-start', - '-webkit-min-device-pixel-ratio': 'min-resolution', - '-webkit-max-device-pixel-ratio': 'max-resolution' -} - function reject(reason?: any) { throw new Error(reason ?? 'Parsing aborted'); @@ -239,12 +197,14 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr const iter: Generator = tokenize(iterator); let item: TokenizeResult; + const rawTokens: TokenizeResult[] = []; while (item = iter.next().value) { stats.bytesIn = item.bytesIn; - // + rawTokens.push(item); + // doParse error if (item.hint != null && BadTokensTypes.includes(item.hint)) { @@ -259,7 +219,8 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr if (item.token == ';' || item.token == '{') { - let node: AstAtRule | AstRule | AstKeyFrameRule | AstInvalidRule | null = await parseNode(tokens, context, stats, options, errors, src, map); + let node: AstAtRule | AstRule | AstKeyFrameRule | AstInvalidRule | null = await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (node != null) { @@ -296,7 +257,9 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr map = new Map; } else if (item.token == '}') { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; + const previousNode = stack.pop() as AstNode; // @ts-ignore @@ -328,7 +291,8 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr if (tokens.length > 0) { - await parseNode(tokens, context, stats, options, errors, src, map); + await parseNode(tokens, context, stats, options, errors, src, map, rawTokens); + rawTokens.length = 0; if (context != null && context.typ == EnumToken.InvalidRuleTokenType) { @@ -472,10 +436,27 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr } } +function getLastNode(context: AstRuleList | AstInvalidRule | AstInvalidAtRule): AstNode | null { + + let i: number = (context.chi as AstNode[]).length; + + while (i--) { + + if ([EnumToken.CommentTokenType, EnumToken.CDOCOMMTokenType, EnumToken.WhitespaceTokenType].includes((context.chi as AstNode[])[i].typ)) { + + continue; + } + + return (context.chi as AstNode[])[i]; + } + + return null; +} + async function parseNode(results: TokenizeResult[], context: AstRuleList | AstInvalidRule | AstInvalidAtRule, stats: { bytesIn: number; importedBytesIn: number; -}, options: ParserOptions, errors: ErrorDescription[], src: string, map: Map): Promise { +}, options: ParserOptions, errors: ErrorDescription[], src: string, map: Map, rawTokens: TokenizeResult[]): Promise { let tokens: Token[] = []; @@ -555,25 +536,6 @@ async function parseNode(results: TokenizeResult[], context: AstRuleList | AstIn const atRule: AtRuleToken = tokens.shift(); const position: Position = map.get(atRule); - // if (atRule.val == 'charset') { - // - // if (context.typ != EnumToken.StyleSheetNodeType || context.chi.some(t => t.typ != EnumToken.CDOCOMMTokenType && t.typ != EnumToken.CommentNodeType)) { - // - // errors.push({ - // action: 'drop', - // message: 'doParse: invalid @charset', - // location: {src, ...position} - // }); - // - // return null; - // } - // - // if (options.removeCharset) { - // - // return null; - // } - // } - // @ts-ignore while ([EnumToken.WhitespaceTokenType].includes(tokens[0]?.typ)) { tokens.shift(); @@ -698,11 +660,59 @@ async function parseNode(results: TokenizeResult[], context: AstRuleList | AstIn // https://www.w3.org/TR/css-nesting-1/#conditionals // allowed nesting at-rules // there must be a top level rule in the stack + if (atRule.val == 'charset') { + let spaces: number = 0; - if (atRule.val == 'charset' && options.removeCharset) { + // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset + for (let k = 1; k < rawTokens.length; k++) { - return null; + if (rawTokens[k].hint == EnumToken.WhitespaceTokenType) { + + spaces+= rawTokens[k].len; + continue; + } + + if (rawTokens[k].hint == EnumToken.CommentTokenType) { + + continue; + } + + if (rawTokens[k].hint == EnumToken.CDOCOMMTokenType) { + continue; + } + + if (spaces > 1) { + + errors.push({ + action: 'drop', + message: '@charset must have only one space', + // @ts-ignore + location: {src, ...(map.get(atRule) ?? position)} + }); + + return null; + } + + if (rawTokens[k].hint != EnumToken.StringTokenType || rawTokens[k].token[0] != '"') { + + errors.push({ + action: 'drop', + message: '@charset expects a ""', + // @ts-ignore + location: {src, ...(map.get(atRule) ?? position)} + }); + + return null; + } + + break; + } + + if (options.removeCharset) { + + return null; + } } const t = parseAtRulePrelude(parseTokens(tokens, {minify: options.minify}), atRule) as Token[]; @@ -741,12 +751,47 @@ async function parseNode(results: TokenizeResult[], context: AstRuleList | AstIn if (options.validation) { - const valid: ValidationResult = validateAtRule(node, options, context); + let isValid: boolean = true; + + if (node.nam == 'else') { + + const prev = getLastNode(context); + + if (prev != null && prev.typ == EnumToken.AtRuleNodeType && ['when', 'else'].includes((prev).nam)) { + + if ((prev).nam == 'else') { + + isValid = Array.isArray((prev).tokens) && ((prev).tokens as Token[]).length > 0; + } + } else { + + isValid = false; + } + } + + const valid: ValidationResult = isValid ? validateAtRule(node, options, context) : { + valid: ValidationLevel.Drop, + node, + matches: [] as Token[], + syntax: '@' + node.nam, + error: '@' + node.nam + ' not allowed here', + tokens + } as ValidationResult; if (valid.valid == ValidationLevel.Drop) { + errors.push({ + action: 'drop', + message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, {minify: false}), '') + '"', + // @ts-ignore + location: {src, ...(map.get(valid.node) ?? position)} + }); + // @ts-ignore node.typ = EnumToken.InvalidAtRuleTokenType; + } else { + + node.val = (node.tokens as Token[]).reduce((acc, curr) => acc + renderToken(curr, {minify: false, removeComments: true}), ''); } } @@ -1113,7 +1158,7 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ } } - if (value.typ == EnumToken.ParensTokenType) { + if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes((value).val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); @@ -1122,6 +1167,8 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ let nameIndex: number = -1; let valueIndex: number = -1; + const dashedIdent: boolean = value.typ == EnumToken.FunctionTokenType && value.val == 'style'; + for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { @@ -1129,7 +1176,7 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ continue; } - if (value.chi[i].typ == EnumToken.IdenTokenType) { + if ((dashedIdent && value.chi[i].typ == EnumToken.DashedIdenTokenType) || value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { nameIndex = i; } @@ -1172,6 +1219,15 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ const val = value.chi.splice(valueIndex, 1)[0] as Token; const node = value.chi.splice(nameIndex, 1)[0] as IdentToken; + // 'background' + // @ts-ignore + if (node.typ == EnumToken.ColorTokenType && (node as ColorToken).kin == 'dpsys') { + + // @ts-ignore + delete node.kin; + node.typ = EnumToken.IdenTokenType; + } + while (value.chi[0]?.typ == EnumToken.WhitespaceTokenType) { value.chi.shift(); diff --git a/src/lib/parser/tokenize.ts b/src/lib/parser/tokenize.ts index 802876f6..09a29841 100644 --- a/src/lib/parser/tokenize.ts +++ b/src/lib/parser/tokenize.ts @@ -24,10 +24,9 @@ function consumeWhiteSpace(parseInfo: ParseInfo): number { return count; } - function pushToken(token: string, parseInfo: ParseInfo, hint?: EnumToken): TokenizeResult { - const result = {token, hint, position: {...parseInfo.position}, bytesIn: parseInfo.currentPosition.ind + 1}; + const result = {token, len: parseInfo.currentPosition.ind - parseInfo.position.ind, hint, position: {...parseInfo.position}, bytesIn: parseInfo.currentPosition.ind + 1}; parseInfo.position.ind = parseInfo.currentPosition.ind; parseInfo.position.lin = parseInfo.currentPosition.lin; @@ -264,13 +263,17 @@ export function* tokenize(stream: InputStream): Generator { buffer = ''; break; } + } else { buffer += value; } } - yield pushToken(buffer, parseInfo, EnumToken.BadCommentTokenType); - buffer = ''; + if (buffer.length > 0) { + + yield pushToken(buffer, parseInfo, EnumToken.BadCommentTokenType); + buffer = ''; + } } break; diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index 453ff2f1..08328a0a 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -100,16 +100,23 @@ function update(position: Position, str: string) { export function doRender(data: AstNode, options: RenderOptions = {}): RenderResult { + const minify: boolean = options.minify ?? true; + const beautify: boolean = options.beautify ?? !minify; options = { - ...(options.minify ?? true ? { + ...(beautify ? { + + indent: ' ', + newLine: '\n', + } : { + indent: '', newLine: '', + }), + ...(minify ? { removeEmpty: true, removeComments: true } : { - indent: ' ', - newLine: '\n', - compress: false, + removeEmpty: false, removeComments: false, }), sourcemap: false, convertColor: true, expandNestingRules: false, preserveLicense: false, ...options diff --git a/src/lib/syntax/syntax.ts b/src/lib/syntax/syntax.ts index 036e5bce..ded42dcf 100644 --- a/src/lib/syntax/syntax.ts +++ b/src/lib/syntax/syntax.ts @@ -36,6 +36,345 @@ export const mediaTypes: string[] = ['all', 'print', 'screen', // https://www.w3.org/TR/css-values-4/#math-function export const mathFuncs: string[] = ['calc', 'clamp', 'min', 'max', 'round', 'mod', 'rem', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'pow', 'sqrt', 'hypot', 'log', 'exp', 'abs', 'sign']; +export const webkitPseudoAliasMap: Record = { + '-webkit-autofill': 'autofill', + '-webkit-any': 'is', + '-moz-any': 'is', + '-webkit-border-after': 'border-block-end', + '-webkit-border-after-color': 'border-block-end-color', + '-webkit-border-after-style': 'border-block-end-style', + '-webkit-border-after-width': 'border-block-end-width', + '-webkit-border-before': 'border-block-start', + '-webkit-border-before-color': 'border-block-start-color', + '-webkit-border-before-style': 'border-block-start-style', + '-webkit-border-before-width': 'border-block-start-width', + '-webkit-border-end': 'border-inline-end', + '-webkit-border-end-color': 'border-inline-end-color', + '-webkit-border-end-style': 'border-inline-end-style', + '-webkit-border-end-width': 'border-inline-end-width', + '-webkit-border-start': 'border-inline-start', + '-webkit-border-start-color': 'border-inline-start-color', + '-webkit-border-start-style': 'border-inline-start-style', + '-webkit-border-start-width': 'border-inline-start-width', + '-webkit-box-align': 'align-items', + '-webkit-box-direction': 'flex-direction', + '-webkit-box-flex': 'flex-grow', + '-webkit-box-lines': 'flex-flow', + '-webkit-box-ordinal-group': 'order', + '-webkit-box-orient': 'flex-direction', + '-webkit-box-pack': 'justify-content', + '-webkit-column-break-after': 'break-after', + '-webkit-column-break-before': 'break-before', + '-webkit-column-break-inside': 'break-inside', + '-webkit-font-feature-settings': 'font-feature-settings', + '-webkit-hyphenate-character': 'hyphenate-character', + '-webkit-initial-letter': 'initial-letter', + '-webkit-margin-end': 'margin-block-end', + '-webkit-margin-start': 'margin-block-start', + '-webkit-padding-after': 'padding-block-end', + '-webkit-padding-before': 'padding-block-start', + '-webkit-padding-end': 'padding-inline-end', + '-webkit-padding-start': 'padding-inline-start', + '-webkit-min-device-pixel-ratio': 'min-resolution', + '-webkit-max-device-pixel-ratio': 'max-resolution' +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions +// https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar +export const webkitExtensions = new Set([ + '-webkit-app-region', + '-webkit-border-horizontal-spacing', + '-webkit-border-vertical-spacing', + '-webkit-box-reflect', + '-webkit-column-axis', + '-webkit-column-progression', + '-webkit-cursor-visibility', + '-webkit-font-smoothing', + '-webkit-hyphenate-limit-after', + '-webkit-hyphenate-limit-before', + '-webkit-hyphenate-limit-lines', + '-webkit-line-align', + '-webkit-line-box-contain', + '-webkit-line-clamp', + '-webkit-line-grid', + '-webkit-line-snap', + '-webkit-locale', + '-webkit-logical-height', + '-webkit-logical-width', + '-webkit-margin-after', + '-webkit-margin-before', + '-webkit-mask-box-image-outset', + '-webkit-mask-box-image-repeat', + '-webkit-mask-box-image-slice', + '-webkit-mask-box-image-source', + '-webkit-mask-box-image-width', + '-webkit-mask-box-image', + '-webkit-mask-composite', + '-webkit-mask-position-x', + '-webkit-mask-position-y', + '-webkit-mask-repeat-x', + '-webkit-mask-repeat-y', + '-webkit-mask-source-type', + '-webkit-max-logical-height', + '-webkit-max-logical-width', + '-webkit-min-logical-height', + '-webkit-min-logical-width', + '-webkit-nbsp-mode', + '-webkit-perspective-origin-x', + '-webkit-perspective-origin-y', + '-webkit-rtl-ordering', + '-webkit-tap-highlight-color', + '-webkit-text-decoration-skip', + '-webkit-text-decorations-in-effect', + '-webkit-text-fill-color', + '-webkit-text-security', + '-webkit-text-stroke-color', + '-webkit-text-stroke-width', + '-webkit-text-stroke', + '-webkit-text-zoom', + '-webkit-touch-callout', + '-webkit-transform-origin-x', + '-webkit-transform-origin-y', + '-webkit-transform-origin-z', + '-webkit-user-drag', + '-webkit-user-modify', + '-webkit-border-after', + '-webkit-border-after-color', + '-webkit-border-after-style', + '-webkit-border-after-width', + '-webkit-border-before', + '-webkit-border-before-color', + '-webkit-border-before-style', + '-webkit-border-before-width', + '-webkit-border-end', + '-webkit-border-end-color', + '-webkit-border-end-style', + '-webkit-border-end-width', + '-webkit-border-start', + '-webkit-border-start-color', + '-webkit-border-start-style', + '-webkit-border-start-width', + '-webkit-box-align', + '-webkit-box-direction', + '-webkit-box-flex-group', + '-webkit-box-flex', + '-webkit-box-lines', + '-webkit-box-ordinal-group', + '-webkit-box-orient', + '-webkit-box-pack', + '-webkit-column-break-after', + '-webkit-column-break-before', + '-webkit-column-break-inside', + '-webkit-font-feature-settings', + '-webkit-hyphenate-character', + '-webkit-initial-letter', + '-webkit-margin-end', + '-webkit-margin-start', + '-webkit-padding-after', + '-webkit-padding-before', + '-webkit-padding-end', + '-webkit-padding-start', + '-webkit-fill-available', + ':-webkit-animating-full-screen-transition', + ':-webkit-any', + ':-webkit-any-link', + ':-webkit-autofill', + ':-webkit-autofill-strong-password', + ':-webkit-drag', + ':-webkit-full-page-media', + ':-webkit-full-screen*', + ':-webkit-full-screen-ancestor', + ':-webkit-full-screen-document', + ':-webkit-full-screen-controls-hidden', + '::-webkit-file-upload-button*', + '::-webkit-inner-spin-button', + '::-webkit-input-placeholder', + '::-webkit-meter-bar', + '::-webkit-meter-even-less-good-value', + '::-webkit-meter-inner-element', + '::-webkit-meter-optimum-value', + '::-webkit-meter-suboptimum-value', + '::-webkit-progress-bar', + '::-webkit-progress-inner-element', + '::-webkit-progress-value', + '::-webkit-search-cancel-button', + '::-webkit-search-results-button', + '::-webkit-slider-runnable-track', + '::-webkit-slider-thumb', + '-webkit-animation', + '-webkit-device-pixel-ratio', + '-webkit-transform-2d', + '-webkit-transform-3d', + '-webkit-transition', + '::-webkit-scrollbar', + '::-webkit-scrollbar-button', + '::-webkit-scrollbar', + '::-webkit-scrollbar-thumb', + '::-webkit-scrollbar-track', + '::-webkit-scrollbar-track-piece', + '::-webkit-scrollbar:vertical', + '::-webkit-scrollbar-corner ', + '::-webkit-resizer', + ':vertical', + ':horizontal', +]); + +// https://developer.mozilla.org/en-US/docs/Web/CSS/Mozilla_Extensions +export const mozExtensions = new Set([ + '-moz-box-align', + '-moz-box-direction', + '-moz-box-flex', + '-moz-box-ordinal-group', + '-moz-box-orient', + '-moz-box-pack', + '-moz-float-edge', + '-moz-force-broken-image-icon', + '-moz-image-region', + '-moz-orient', + '-moz-osx-font-smoothing', + '-moz-user-focus', + '-moz-user-input', + '-moz-user-modify', + '-moz-animation', + '-moz-animation-delay', + '-moz-animation-direction', + '-moz-animation-duration', + '-moz-animation-fill-mode', + '-moz-animation-iteration-count', + '-moz-animation-name', + '-moz-animation-play-state', + '-moz-animation-timing-function', + '-moz-appearance', + '-moz-backface-visibility', + '-moz-background-clip', + '-moz-background-origin', + '-moz-background-inline-policy', + '-moz-background-size', + '-moz-border-end', + '-moz-border-end-color', + '-moz-border-end-style', + '-moz-border-end-width', + '-moz-border-image', + '-moz-border-start', + '-moz-border-start-color', + '-moz-border-start-style', + '-moz-border-start-width', + '-moz-box-sizing', + 'clip-path', + '-moz-column-count', + '-moz-column-fill', + '-moz-column-gap', + '-moz-column-width', + '-moz-column-rule', + '-moz-column-rule-width', + '-moz-column-rule-style', + '-moz-column-rule-color', + 'filter', + '-moz-font-feature-settings', + '-moz-font-language-override', + '-moz-hyphens', + '-moz-margin-end', + '-moz-margin-start', + 'mask', + '-moz-opacity', + '-moz-outline', + '-moz-outline-color', + '-moz-outline-offset', + '-moz-outline-style', + '-moz-outline-width', + '-moz-padding-end', + '-moz-padding-start', + '-moz-perspective', + '-moz-perspective-origin', + 'pointer-events', + '-moz-tab-size', + '-moz-text-align-last', + '-moz-text-decoration-color', + '-moz-text-decoration-line', + '-moz-text-decoration-style', + '-moz-text-size-adjust', + '-moz-transform', + '-moz-transform-origin', + '-moz-transform-style', + '-moz-transition', + '-moz-transition-delay', + '-moz-transition-duration', + '-moz-transition-property', + '-moz-transition-timing-function', + '-moz-user-select', + '-moz-initial', + '-moz-appearance', + '-moz-linear-gradient', + '-moz-radial-gradient', + '-moz-element', + '-moz-image-rect', + '::-moz-anonymous-block', + '::-moz-anonymous-positioned-block', + ':-moz-any', + ':-moz-any-link', + ':-moz-broken', + '::-moz-canvas', + '::-moz-color-swatch', + '::-moz-cell-content', + ':-moz-drag-over', + ':-moz-first-node', + '::-moz-focus-inner', + '::-moz-focus-outer', + ':-moz-full-screen', + ':-moz-full-screen-ancestor', + ':-moz-handler-blocked', + ':-moz-handler-crashed', + ':-moz-handler-disabled', + '::-moz-inline-table', + ':-moz-last-node', + '::-moz-list-bullet', + '::-moz-list-number', + ':-moz-loading', + ':-moz-locale-dir', + ':-moz-locale-dir', + ':-moz-lwtheme', + ':-moz-lwtheme-brighttext', + ':-moz-lwtheme-darktext', + '::-moz-meter-bar', + ':-moz-native-anonymous', + ':-moz-only-whitespace', + '::-moz-pagebreak', + '::-moz-pagecontent', + ':-moz-placeholder', + '::-moz-placeholder', + '::-moz-progress-bar', + '::-moz-range-progress', + '::-moz-range-thumb', + '::-moz-range-track', + ':-moz-read-only', + ':-moz-read-write', + '::-moz-scrolled-canvas', + '::-moz-scrolled-content', + '::-moz-selection', + ':-moz-submit-invalid', + ':-moz-suppressed', + '::-moz-svg-foreign-content', + '::-moz-table', + '::-moz-table-cell', + '::-moz-table-column', + '::-moz-table-column-group', + '::-moz-table-outer', + '::-moz-table-row', + '::-moz-table-row-group', + ':-moz-ui-invalid', + ':-moz-ui-valid', + ':-moz-user-disabled', + '::-moz-viewport', + '::-moz-viewport-scroll', + ':-moz-window-inactive', + '-moz-device-pixel-ratio', + '-moz-os-version', + '-moz-touch-enabled', + '-moz-windows-glass', + '-moz-alt-content' + ] +); + export function isLength(dimension: DimensionToken): boolean { return 'unit' in dimension && dimensionUnits.has(dimension.unit.toLowerCase()); @@ -706,4 +1045,4 @@ export function isWhiteSpace(codepoint: number): boolean { return codepoint == 0x9 || codepoint == 0x20 || // isNewLine codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; -} \ No newline at end of file +} diff --git a/src/lib/validation/at-rules/container.ts b/src/lib/validation/at-rules/container.ts new file mode 100644 index 00000000..00f63804 --- /dev/null +++ b/src/lib/validation/at-rules/container.ts @@ -0,0 +1,451 @@ +import type {AstAtRule, AstNode, MediaFeatureNotToken, Token, ValidationOptions} from "../../../@types"; +import type {ValidationSyntaxResult} from "../../../@types/validation"; +import {EnumToken, ValidationLevel} from "../../ast"; +import {consumeWhitespace, splitTokenList} from "../utils"; + +const validateContainerScrollStateFeature = validateContainerSizeFeature; + +export function validateAtRuleContainer(atRule: AstAtRule, options: ValidationOptions, root?: AstNode): ValidationSyntaxResult { + + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + } as ValidationSyntaxResult; + } + + const result: ValidationSyntaxResult = validateAtRuleContainerQueryList(atRule.tokens, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + + if (!('chi' in atRule)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + } as ValidationSyntaxResult; + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + } as ValidationSyntaxResult; +} + +function validateAtRuleContainerQueryList(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + if (tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } as ValidationSyntaxResult; + } + + let result: ValidationSyntaxResult | null = null; + let tokenType: EnumToken | null = null; + + for (const queries of splitTokenList(tokens)) { + + consumeWhitespace(queries); + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } + } + + result = null; + const match: Token[] = []; + let token: Token | null = null; + + tokenType = null; + + while (queries.length > 0) { + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } + } + + if (queries[0].typ == EnumToken.IdenTokenType) { + + match.push(queries.shift() as Token); + consumeWhitespace(queries); + } + + if (queries.length == 0) { + + break; + } + + token = queries[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + token = token.val; + } + + if (token.typ != EnumToken.ParensTokenType && (token.typ != EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + + if (token.typ == EnumToken.ParensTokenType) { + + result = validateContainerSizeFeature(token.chi, atRule); + } else if (token.val == 'scroll-state') { + + result = validateContainerScrollStateFeature(token.chi, atRule); + } else { + + result = validateContainerStyleFeature(token.chi, atRule); + } + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + + queries.shift(); + consumeWhitespace(queries); + + if (queries.length == 0) { + + break; + } + + token = queries[0]; + + if (token.typ != EnumToken.MediaFeatureAndTokenType && token.typ != EnumToken.MediaFeatureOrTokenType) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + } + } + + if (tokenType == null) { + + tokenType = token.typ; + } + + if (tokenType != token.typ) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + } + } + + queries.shift(); + consumeWhitespace(queries); + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } +} + +function validateContainerStyleFeature(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + + consumeWhitespace(tokens); + + if (tokens.length == 1) { + + if (tokens[0].typ == EnumToken.ParensTokenType) { + + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + + if ([EnumToken.DashedIdenTokenType, EnumToken.IdenTokenType].includes(tokens[0].typ) || + ( tokens[0].typ == EnumToken.MediaQueryConditionTokenType &&tokens[0].op.typ == EnumToken.ColonTokenType)) { + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } + } + } + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } +} + +function validateContainerSizeFeature(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + if (tokens.length == 1) { + + const token: Token = tokens[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + return validateContainerSizeFeature([(token as MediaFeatureNotToken).val], atRule); + } + + if (token.typ == EnumToken.ParensTokenType) { + + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + + if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } + } + + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); +} + +function validateAtRuleContainerQueryStyleInParams(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + let token: Token = tokens[0]; + let tokenType: EnumToken | null = null; + let result: ValidationSyntaxResult | null = null; + + while (tokens.length > 0) { + + token = tokens[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + token = token.val; + } + + if (tokens[0].typ != EnumToken.ParensTokenType) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + + const slices = tokens[0].chi.slice(); + + consumeWhitespace(slices); + + if (slices.length == 1) { + + if ([EnumToken.MediaFeatureNotTokenType, EnumToken.ParensTokenType].includes(slices[0].typ)) { + + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + } else if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + } else { + + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + } + + tokens.shift(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + break; + } + + if (![EnumToken.MediaFeatureAndTokenType, EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + } + } + + if (tokenType == null) { + + tokenType = tokens[0].typ; + } + + if (tokenType != tokens[0].typ) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + } + } + + tokens.shift(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } +} \ No newline at end of file diff --git a/src/lib/validation/at-rules/counter-style.ts b/src/lib/validation/at-rules/counter-style.ts index dde7134a..a168f03e 100644 --- a/src/lib/validation/at-rules/counter-style.ts +++ b/src/lib/validation/at-rules/counter-style.ts @@ -13,7 +13,7 @@ export function validateAtRuleCounterStyle(atRule: AstAtRule, options: Validatio matches: [], node: atRule, syntax: '@counter-style', - error: 'expected media query list', + error: 'expected counter style name', tokens: [] } as ValidationSyntaxResult; } @@ -24,7 +24,7 @@ export function validateAtRuleCounterStyle(atRule: AstAtRule, options: Validatio // @ts-ignore return { - valid: ValidationLevel.Valid, + valid: ValidationLevel.Drop, matches: [], node: atRule, syntax: '@counter-style', diff --git a/src/lib/validation/at-rules/custom-media.ts b/src/lib/validation/at-rules/custom-media.ts new file mode 100644 index 00000000..bb9bffcf --- /dev/null +++ b/src/lib/validation/at-rules/custom-media.ts @@ -0,0 +1,58 @@ +import type {AstAtRule, AstNode, Token, ValidationOptions} from "../../../@types"; +import type {ValidationSyntaxResult} from "../../../@types/validation"; +import {EnumToken, ValidationLevel} from "../../ast"; +import {consumeWhitespace} from "../utils"; +import {validateAtRuleMediaQueryList} from "./media"; + +export function validateAtRuleCustomMedia(atRule: AstAtRule, options: ValidationOptions, root?: AstNode): ValidationSyntaxResult { + + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + } as ValidationSyntaxResult; + } + + const queries: Token[] = atRule.tokens.slice(); + + consumeWhitespace(queries); + + if (queries.length == 0 || queries[0].typ != EnumToken.DashedIdenTokenType) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@custom-media', + error: 'expecting dashed identifier', + tokens: [] + } + } + + queries.shift(); + + const result: ValidationSyntaxResult = validateAtRuleMediaQueryList(queries, atRule); + + if (result.valid == ValidationLevel.Drop) { + + atRule.tokens = []; + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@custom-media', + error: '', + tokens: [] + } + } + + return result; +} \ No newline at end of file diff --git a/src/lib/validation/at-rules/else.ts b/src/lib/validation/at-rules/else.ts new file mode 100644 index 00000000..8c15e8fb --- /dev/null +++ b/src/lib/validation/at-rules/else.ts @@ -0,0 +1,4 @@ +import {validateAtRuleWhen} from "./when"; + + +export const validateAtRuleElse = validateAtRuleWhen; \ No newline at end of file diff --git a/src/lib/validation/at-rules/index.ts b/src/lib/validation/at-rules/index.ts index fc42de2a..2637115c 100644 --- a/src/lib/validation/at-rules/index.ts +++ b/src/lib/validation/at-rules/index.ts @@ -8,4 +8,7 @@ export * from './layer'; export * from './font-feature-values'; export * from './namespace'; export * from './document'; -export * from './keyframes'; \ No newline at end of file +export * from './keyframes'; +export * from './when'; +export * from './else'; +export * from './container'; \ No newline at end of file diff --git a/src/lib/validation/at-rules/media.ts b/src/lib/validation/at-rules/media.ts index ccf9771a..b49ddddf 100644 --- a/src/lib/validation/at-rules/media.ts +++ b/src/lib/validation/at-rules/media.ts @@ -1,4 +1,12 @@ -import type {AstAtRule, AstNode, Token, ValidationOptions} from "../../../@types"; +import { + AstAtRule, + AstNode, + FunctionToken, + MediaFeatureToken, + ParensToken, + Token, + ValidationOptions +} from "../../../@types"; import type {ValidationSyntaxResult} from "../../../@types/validation.d.ts"; import {EnumToken, ValidationLevel} from "../../ast"; import {consumeWhitespace, splitTokenList} from "../utils"; @@ -10,16 +18,34 @@ export function validateAtRuleMedia(atRule: AstAtRule, options: ValidationOption // @ts-ignore return { - valid: ValidationLevel.Drop, + valid: ValidationLevel.Valid, + matches: [], + node: null, + syntax: null, + error: '', + tokens: [] + } as ValidationSyntaxResult; + } + + let result: ValidationSyntaxResult | null = null; + + const slice: Token[] = atRule.tokens.slice(); + + consumeWhitespace(slice); + + if (slice.length == 0) { + + return { + valid: ValidationLevel.Valid, matches: [], node: atRule, syntax: '@media', - error: 'expected media query list', + error: '', tokens: [] - } as ValidationSyntaxResult; + } } - const result: ValidationSyntaxResult = validateAtRuleMediaQueryList(atRule.tokens, atRule); + result = validateAtRuleMediaQueryList(atRule.tokens, atRule); if (result.valid == ValidationLevel.Drop) { @@ -52,12 +78,25 @@ export function validateAtRuleMedia(atRule: AstAtRule, options: ValidationOption export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRule): ValidationSyntaxResult { - for (const tokens of splitTokenList(tokenList)) { + const split: Token[][] = splitTokenList(tokenList); + const matched: Token[][] = []; + let result: ValidationSyntaxResult | null = null; + let previousToken: Token | null; + let mediaFeatureType: Token | null; + + for (let i = 0; i < split.length; i++) { + + const tokens: Token[] = split[i].slice(); + const match: Token[] = []; + + result = null; + mediaFeatureType = null; + previousToken = null; if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -65,25 +104,42 @@ export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRu error: 'unexpected token', tokens: [] } as ValidationSyntaxResult; + continue; } - let previousToken: Token | null = null; - while (tokens.length > 0) { - // media-condition - if (validateMediaCondition(tokens[0])) { + previousToken = tokens[0]; - previousToken = tokens[0]; - tokens.shift(); - } - // media-type - else if (validateMediaFeature(tokens[0])) { + // media-condition | media-type | custom-media + if (!(validateMediaCondition(tokens[0], atRule) || validateMediaFeature(tokens[0]) || validateCustomMediaCondition(tokens[0], atRule))) { + + if (tokens[0].typ == EnumToken.ParensTokenType) { + + result = validateAtRuleMediaQueryList(tokens[0].chi, atRule); + } + else { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0] ?? atRule, + syntax: '@media', + error: 'expecting media feature or media condition', + tokens: [] + } + } + + if (result.valid == ValidationLevel.Drop) { - previousToken = tokens[0]; - tokens.shift(); + break; + } + + result = null; } + match.push(tokens.shift() as Token); + if (tokens.length == 0) { break; @@ -94,7 +150,7 @@ export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRu if (previousToken?.typ != EnumToken.ParensTokenType) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -102,13 +158,15 @@ export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRu error: 'expected media query list', tokens: [] } + + break; } } - if (![EnumToken.MediaFeatureOrTokenType, EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { + else if (![EnumToken.MediaFeatureOrTokenType, EnumToken.MediaFeatureAndTokenType].includes(tokens[0].typ)) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, @@ -116,38 +174,100 @@ export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRu error: 'expected and/or', tokens: [] } + + break; } - if (tokens.length == 1) { + if (mediaFeatureType == null) { + + mediaFeatureType = tokens[0]; + } + + if (mediaFeatureType.typ != tokens[0].typ) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected media-condition', + error: 'mixing and/or not allowed at the same level', tokens: [] } + + break; } - tokens.shift(); + match.push({typ: EnumToken.WhitespaceTokenType}, tokens.shift() as Token); - if (!consumeWhitespace(tokens)) { + consumeWhitespace(tokens); + + if (tokens.length == 0) { // @ts-ignore - return { + result = { valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, syntax: '@media', - error: 'expected whitespace', + error: 'expected media-condition', tokens: [] } + + break; } + + match.push({typ: EnumToken.WhitespaceTokenType}); + } + + if (result == null && match.length > 0) { + + matched.push(match); } } + if (result != null) { + + return result; + } + + if (matched.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@media', + error: 'expected media query list', + tokens: [] + }; + } + + tokenList.length = 0; + + let hasAll: boolean = false; + + for (let i = 0; i < matched.length; i++) { + + if (tokenList.length > 0) { + + tokenList.push({typ: EnumToken.CommaTokenType}); + } + + if (matched[i].length == 1 && matched.length > 1 && matched[i][0].typ == EnumToken.MediaFeatureTokenType && (matched[i][0] as MediaFeatureToken).val == 'all') { + + hasAll = true; + continue; + } + + tokenList.push(...matched[i]); + } + + if (hasAll && tokenList.length == 0) { + + tokenList.push({typ: EnumToken.MediaFeatureTokenType, val: 'all'}); + } + // @ts-ignore return { valid: ValidationLevel.Valid, @@ -159,11 +279,11 @@ export function validateAtRuleMediaQueryList(tokenList: Token[], atRule: AstAtRu } } -function validateMediaCondition(token: Token): boolean { +function validateCustomMediaCondition(token: Token, atRule: AstAtRule): boolean { if (token.typ == EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(token.val); + return validateMediaCondition(token.val, atRule); } if (token.typ != EnumToken.ParensTokenType) { @@ -178,6 +298,28 @@ function validateMediaCondition(token: Token): boolean { return false; } + return chi[0].typ == EnumToken.DashedIdenTokenType; +} + +export function validateMediaCondition(token: Token, atRule: AstAtRule): boolean { + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + return validateMediaCondition(token.val, atRule); + } + + if (token.typ != EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == EnumToken.FunctionTokenType && ['media', 'supports'].includes(token.val)) ) { + + return false; + } + + const chi: Token[] = (token as ParensToken | FunctionToken).chi.filter((t: Token): boolean => t.typ != EnumToken.CommentTokenType && t.typ != EnumToken.WhitespaceTokenType); + + if (chi.length != 1) { + + return false; + } + if (chi[0].typ == EnumToken.IdenTokenType) { return true; @@ -185,7 +327,7 @@ function validateMediaCondition(token: Token): boolean { if (chi[0].typ == EnumToken.MediaFeatureNotTokenType) { - return validateMediaCondition(chi[0].val); + return validateMediaCondition(chi[0].val, atRule); } if (chi[0].typ == EnumToken.MediaQueryConditionTokenType) { @@ -196,7 +338,7 @@ function validateMediaCondition(token: Token): boolean { return false; } -function validateMediaFeature(token: Token): boolean { +export function validateMediaFeature(token: Token): boolean { let val: Token = token; diff --git a/src/lib/validation/at-rules/supports.ts b/src/lib/validation/at-rules/supports.ts index b8c69be5..aab4a503 100644 --- a/src/lib/validation/at-rules/supports.ts +++ b/src/lib/validation/at-rules/supports.ts @@ -17,7 +17,7 @@ export function validateAtRuleSupports(atRule: AstAtRule, options: ValidationOpt valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports query list', tokens: [] } as ValidationSyntaxResult; @@ -42,7 +42,7 @@ export function validateAtRuleSupports(atRule: AstAtRule, options: ValidationOpt valid: ValidationLevel.Drop, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected at-rule body', tokens: [] } @@ -53,7 +53,7 @@ export function validateAtRuleSupports(atRule: AstAtRule, options: ValidationOpt valid: ValidationLevel.Valid, matches: [], node: atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] } @@ -70,7 +70,7 @@ export function validateAtRuleSupportsConditions(atRule: AstAtRule, tokenList: T valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'unexpected token', tokens: [] } as ValidationSyntaxResult; @@ -116,7 +116,7 @@ export function validateAtRuleSupportsConditions(atRule: AstAtRule, tokenList: T valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? previousToken ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] } @@ -130,7 +130,7 @@ export function validateAtRuleSupportsConditions(atRule: AstAtRule, tokenList: T valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected and/or', tokens: [] } @@ -143,7 +143,7 @@ export function validateAtRuleSupportsConditions(atRule: AstAtRule, tokenList: T valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports-condition', tokens: [] } @@ -158,7 +158,7 @@ export function validateAtRuleSupportsConditions(atRule: AstAtRule, tokenList: T valid: ValidationLevel.Drop, matches: [], node: tokens[0] ?? atRule, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected whitespace', tokens: [] } @@ -176,14 +176,14 @@ export function validateSupportCondition(atRule: AstAtRule, token: Token): Valid return validateSupportCondition(atRule, token.val); } - if (token.typ != EnumToken.ParensTokenType) { + if (token.typ != EnumToken.ParensTokenType && !(['when', 'else'].includes(atRule.nam) && token.typ == EnumToken.FunctionTokenType && ['supports', 'font-format', 'font-tech'].includes(token.val))) { // @ts-ignore return { valid: ValidationLevel.Drop, matches: [], node: token, - syntax: '@supports', + syntax: '@' + atRule.nam, error: 'expected supports condition-in-parens', tokens: [] }; @@ -202,7 +202,7 @@ export function validateSupportCondition(atRule: AstAtRule, token: Token): Valid valid: ValidationLevel.Valid, matches: [], node: null, - syntax: '@supports', + syntax: '@' + atRule.nam, error: '', tokens: [] }; diff --git a/src/lib/validation/at-rules/when.ts b/src/lib/validation/at-rules/when.ts new file mode 100644 index 00000000..79d427d8 --- /dev/null +++ b/src/lib/validation/at-rules/when.ts @@ -0,0 +1,229 @@ +import {AstAtRule, type AstNode, FunctionToken, Token, type ValidationOptions} from "../../../@types"; +import type {ValidationSyntaxResult} from "../../../@types/validation"; +import {EnumToken, ValidationLevel} from "../../ast"; +import {consumeWhitespace, splitTokenList} from "../utils"; +import {validateMediaCondition, validateMediaFeature} from "./media"; +import {validateSupportCondition} from "./supports"; + +export function validateAtRuleWhen(atRule: AstAtRule, options: ValidationOptions, root?: AstNode): ValidationSyntaxResult { + + const slice: Token[] = Array.isArray(atRule.tokens) ? atRule.tokens.slice() : []; + + consumeWhitespace(slice); + + if (slice.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: [] + } as ValidationSyntaxResult; + } + + const result: ValidationSyntaxResult = validateAtRuleWhenQueryList(atRule.tokens as Token[], atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + + if (!('chi' in atRule)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@when', + error: 'expected at-rule body', + tokens: [] + } as ValidationSyntaxResult; + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: result.tokens + } +} + +// media() = media( [ | | ] ) +// supports() = supports( ) +export function validateAtRuleWhenQueryList(tokenList: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + const matched: Token[][] = []; + + let result: ValidationSyntaxResult | null = null; + + for (const split of splitTokenList(tokenList)) { + + const match: Token[] = []; + result = null; + + consumeWhitespace(split); + + if (split.length == 0) { + + continue; + } + + while (split.length > 0) { + + if (split[0].typ != EnumToken.FunctionTokenType || !['media', 'supports', 'font-tech', 'font-format'].includes((split[0] as FunctionToken).val)) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'unexpected token', + tokens: [] + } as ValidationSyntaxResult; + + break; + } + + const chi: Token[] = split[0].chi.slice() as Token[]; + + consumeWhitespace(chi); + + if (split[0].val == 'media') { + + // result = valida + if (chi.length != 1 || !(validateMediaFeature(chi[0]) || validateMediaCondition(split[0], atRule))) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + } + + break; + } + + } else if (['supports', 'font-tech', 'font-format'].includes(split[0].val) ) { + + // result = valida + if (!validateSupportCondition(atRule, split[0])) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: 'media( [ | | ] )', + error: 'unexpected token', + tokens: [] + } + + break; + } + } + + if (match.length > 0) { + + match.push({typ: EnumToken.WhitespaceTokenType}); + } + + match.push(split.shift() as Token); + consumeWhitespace(split); + + if (split.length == 0) { + + break; + } + + if (![EnumToken.MediaFeatureAndTokenType, EnumToken.MediaFeatureOrTokenType].includes(split[0].typ)) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting and/or media-condition', + tokens: [] + } as ValidationSyntaxResult; + + break; + } + + if (match.length > 0) { + + match.push({typ: EnumToken.WhitespaceTokenType}); + } + + match.push(split.shift() as Token); + + consumeWhitespace(split); + + if (split.length == 0) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: split[0] ?? atRule, + syntax: '@when', + error: 'expecting media-condition', + tokens: [] + } as ValidationSyntaxResult; + + break; + } + } + + if (result == null && match.length > 0) { + + matched.push(match); + } + } + + if (result != null) { + + return result; + } + + if (matched.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: result?.node ?? atRule, + syntax: '@when', + error: 'invalid at-rule body', + tokens: [] + } + } + + tokenList.length = 0; + + for (const match of matched) { + + if (tokenList.length > 0) { + + tokenList.push({ + typ: EnumToken.CommaTokenType + }); + } + + tokenList.push(...match); + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@when', + error: '', + tokens: tokenList + } +} \ No newline at end of file diff --git a/src/lib/validation/atrule.ts b/src/lib/validation/atrule.ts index a99347c8..fa41ac87 100644 --- a/src/lib/validation/atrule.ts +++ b/src/lib/validation/atrule.ts @@ -4,8 +4,10 @@ import {EnumToken, ValidationLevel} from "../ast"; import {getParsedSyntax, getSyntaxConfig} from "./config"; import {ValidationSyntaxGroupEnum, ValidationToken} from "./parser"; import { + validateAtRuleContainer, validateAtRuleCounterStyle, validateAtRuleDocument, + validateAtRuleElse, validateAtRuleFontFeatureValues, validateAtRuleImport, validateAtRuleKeyframes, @@ -14,8 +16,10 @@ import { validateAtRuleNamespace, validateAtRulePage, validateAtRulePageMarginBox, - validateAtRuleSupports + validateAtRuleSupports, + validateAtRuleWhen } from "./at-rules"; +import {validateAtRuleCustomMedia} from "./at-rules/custom-media"; export function validateAtRule(atRule: AstAtRule, options: ValidationOptions, root?: AstNode): ValidationResult { @@ -82,11 +86,31 @@ export function validateAtRule(atRule: AstAtRule, options: ValidationOptions, ro return validateAtRuleNamespace(atRule, options, root); } + if (atRule.nam == 'when') { + + return validateAtRuleWhen(atRule, options, root); + } + + if (atRule.nam == 'else') { + + return validateAtRuleElse(atRule, options, root); + } + + if (atRule.nam == 'container') { + + return validateAtRuleContainer(atRule, options, root); + } + if (atRule.nam == 'document') { return validateAtRuleDocument(atRule, options, root); } + if (atRule.nam == 'custom-media') { + + return validateAtRuleCustomMedia(atRule, options, root); + } + if (['position-try', 'property', 'font-palette-values'].includes(atRule.nam)) { if (!('tokens' in atRule)) { diff --git a/src/lib/validation/selector.ts b/src/lib/validation/selector.ts index cb6e5d38..3893bab6 100644 --- a/src/lib/validation/selector.ts +++ b/src/lib/validation/selector.ts @@ -1,14 +1,14 @@ import type {AstAtRule, AstRule, AstRuleStyleSheet, Token, ValidationOptions} from "../../@types"; import {EnumToken} from "../ast"; -import type {ValidationResult} from "../../@types/validation"; import {validateKeyframeBlockList, validateRelativeSelectorList} from "./syntaxes"; +import type {ValidationResult} from "../../@types/validation"; import {validateSelectorList} from "./syntaxes/selector-list"; export function validateSelector(selector: Token[], options: ValidationOptions, root?: AstAtRule | AstRule | AstRuleStyleSheet): ValidationResult { if (root == null) { - return validateRelativeSelectorList(selector, root); + return validateSelectorList(selector, root); } // @ts-ignore @@ -18,7 +18,6 @@ export function validateSelector(selector: Token[], options: ValidationOptions, } let isNested: number = root.typ == EnumToken.RuleNodeType ? 1 : 0; - let currentRoot = root.parent; while (currentRoot != null && isNested == 0) { diff --git a/src/lib/validation/syntaxes/complex-selector-list.ts b/src/lib/validation/syntaxes/complex-selector-list.ts index c694bde1..a4226548 100644 --- a/src/lib/validation/syntaxes/complex-selector-list.ts +++ b/src/lib/validation/syntaxes/complex-selector-list.ts @@ -1,8 +1,8 @@ import type {AstAtRule, AstRule, Token} from "../../../@types"; import type {ValidationSelectorOptions, ValidationSyntaxResult} from "../../../@types/validation.d.ts"; -import {EnumToken, ValidationLevel} from "../../ast"; +import {ValidationLevel} from "../../ast"; import {validateSelector} from "./selector"; -import {consumeWhitespace} from "../utils"; +import {consumeWhitespace, splitTokenList} from "../utils"; export function validateComplexSelectorList(tokens: Token[], root?: AstAtRule | AstRule, options?: ValidationSelectorOptions): ValidationSyntaxResult { @@ -24,25 +24,26 @@ export function validateComplexSelectorList(tokens: Token[], root?: AstAtRule | } } - let i: number = -1; - let j: number = 0; let result: ValidationSyntaxResult | null = null; - while (i + 1 < tokens.length) { + for (const t of splitTokenList(tokens)) { - if (tokens[++i].typ == EnumToken.CommaTokenType) { - - result = validateSelector(tokens.slice(j, i), root, options); + result = validateSelector(t, root, options); if (result.valid == ValidationLevel.Drop) { return result; } - - j = i + 1; - i = j; - } } - return validateSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + // @ts-ignore + return result ?? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + syntax: null, + error: 'expecting complex selector list', + tokens + }; } \ No newline at end of file diff --git a/src/lib/validation/syntaxes/complex-selector.ts b/src/lib/validation/syntaxes/complex-selector.ts index 3b2c4ed8..d8b23b80 100644 --- a/src/lib/validation/syntaxes/complex-selector.ts +++ b/src/lib/validation/syntaxes/complex-selector.ts @@ -1,10 +1,12 @@ import type {AstAtRule, AstRule, Token} from "../../../@types"; import type {ValidationSelectorOptions, ValidationSyntaxResult} from "../../../@types/validation.d.ts"; -import {consumeWhitespace} from "../utils"; +import {consumeWhitespace, splitTokenList} from "../utils"; import {EnumToken, ValidationLevel} from "../../ast"; +import {validateCompoundSelector} from "./compound-selector"; export const combinatorsTokens: EnumToken[] = [EnumToken.ChildCombinatorTokenType, EnumToken.ColumnCombinatorTokenType, - EnumToken.DescendantCombinatorTokenType, EnumToken.NextSiblingCombinatorTokenType, EnumToken.SubsequentSiblingCombinatorTokenType]; + // EnumToken.DescendantCombinatorTokenType, + EnumToken.NextSiblingCombinatorTokenType, EnumToken.SubsequentSiblingCombinatorTokenType]; // [ ? ]* export function validateComplexSelector(tokens: Token[], root?: AstAtRule | AstRule, options?: ValidationSelectorOptions): ValidationSyntaxResult { @@ -26,313 +28,382 @@ export function validateComplexSelector(tokens: Token[], root?: AstAtRule | AstR } } - while (tokens.length > 0) { + // const config = getSyntaxConfig(); + // + // let match: number = 0; - if (combinatorsTokens.includes(tokens[0].typ)) { + let result: ValidationSyntaxResult | null = null; - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'unexpected combinator', - tokens - } - } - - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - - if (!options?.nestedSelector) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - } - } - - while (tokens.length > 0 && tokens[0].typ == EnumToken.NestingSelectorTokenType) { - - tokens.shift(); - consumeWhitespace(tokens); - } - - if (tokens.length == 0) { - - break; - } - } - - if (EnumToken.IdenTokenType == tokens[0].typ) { - - tokens.shift(); - consumeWhitespace(tokens); - - if (tokens.length == 0) { - - break; - } - } - - if (EnumToken.UniversalSelectorTokenType == tokens[0].typ) { - - tokens.shift(); - consumeWhitespace(tokens); - } - - while (tokens.length > 0) { - - if (tokens[0].typ == EnumToken.PseudoClassFuncTokenType) { - - if (tokens[0].val.startsWith(':-webkit-')) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'invalid pseudo-class', - tokens - } - } - } - - if ([ - EnumToken.ClassSelectorTokenType, - EnumToken.HashTokenType, - EnumToken.PseudoClassTokenType, - EnumToken.PseudoClassFuncTokenType].includes(tokens[0].typ)) { - - tokens.shift(); - consumeWhitespace(tokens); - continue - } - - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - - if (!options?.nestedSelector) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - } - } - - tokens.shift(); - consumeWhitespace(tokens); - continue - } - - // validate namespace - if (tokens[0].typ == EnumToken.NameSpaceAttributeTokenType) { - - if (!((tokens[0].l == null || tokens[0].l.typ == EnumToken.IdenTokenType || (tokens[0].l.typ == EnumToken.LiteralTokenType && tokens[0].l.val == '*')) && - tokens[0].r.typ == EnumToken.IdenTokenType)) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting wq-name', - tokens - } - } + // const combinators: EnumToken[] = combinatorsTokens.filter((t: EnumToken) => t != EnumToken.DescendantCombinatorTokenType); - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - // validate attribute - else if (tokens[0].typ == EnumToken.AttrTokenType) { + for (const t of splitTokenList(tokens, combinatorsTokens)) { - const children: Token[] = tokens[0].chi.slice() as Token[]; + result = validateCompoundSelector(t, root, options); - consumeWhitespace(children); + if (result.valid == ValidationLevel.Drop) { - if (children.length == 0) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - } - } - - if (![ - EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType, - EnumToken.MatchExpressionTokenType].includes(children[0].typ)) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - } - } - - if (children[0].typ == EnumToken.MatchExpressionTokenType) { - - if (![EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType].includes(children[0].l.typ) || - ![ - EnumToken.EqualMatchTokenType, EnumToken.DashMatchTokenType, - EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, - EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType].includes(children[0].op.typ) || - ![EnumToken.StringTokenType, - EnumToken.IdenTokenType].includes(children[0].r.typ)) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - } - } - - if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'invalid attribute selector', - tokens - } - } - } - - children.shift(); - consumeWhitespace(children); - - if (children.length > 0) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: children[0], - syntax: null, - error: 'unexpected token', - tokens - } - } - - tokens.shift(); - consumeWhitespace(tokens); - continue; - } - - break; - } - - if (tokens.length == 0) { - - break + return result; } - // combinator - if (!combinatorsTokens.includes(tokens[0].typ)) { - - if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { - - if (!options?.nestedSelector) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - // @ts-ignore - node: tokens[0], - syntax: null, - error: 'nested selector not allowed', - tokens - } - } - - tokens.shift(); - consumeWhitespace(tokens); - continue - } - - - if (tokens.length > 0 && - [ - EnumToken.IdenTokenType, - EnumToken.AttrTokenType, - EnumToken.NameSpaceAttributeTokenType, - EnumToken.ClassSelectorTokenType, - EnumToken.HashTokenType, - EnumToken.PseudoClassTokenType, - EnumToken.PseudoClassFuncTokenType].includes(tokens[0].typ)) { - - continue - } - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0], - syntax: null, - error: 'expecting combinator or subclass-selector', - tokens - } - } - - const token = tokens.shift() as Token; - consumeWhitespace(tokens); - - if (tokens.length == 0) { - - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax: null, - error: 'expected compound-selector', - tokens - } - } + // if (combinatorsTokens.includes(tokens[0].typ)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'unexpected combinator: ' + JSON.stringify(tokens[0]), + // tokens + // } + // } + // + // else if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { + // + // if (!options?.nestedSelector) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'nested selector not allowed', + // tokens + // } + // + // match++; + // + // tokens.shift(); + // consumeWhitespace(tokens); + // } + // + // while (tokens.length > 0 && tokens[0].typ == EnumToken.NestingSelectorTokenType) { + // + // tokens.shift(); + // consumeWhitespace(tokens); + // } + // + // if (tokens.length == 0) { + // + // break; + // } + // } + // + // else if (EnumToken.IdenTokenType == tokens[0].typ) { + // + // tokens.shift(); + // consumeWhitespace(tokens); + // + // if (tokens.length == 0) { + // + // break; + // } + // } + // + // else if (EnumToken.UniversalSelectorTokenType == tokens[0].typ) { + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue; + // } + // + // while (tokens.length > 0) { + // + // if (tokens[0].typ == EnumToken.PseudoClassFuncTokenType) { + // + // if (tokens[0].val.startsWith(':-webkit-')) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'invalid pseudo-class', + // tokens + // } + // } + // } + // + // if (tokens[0].typ == EnumToken.PseudoClassTokenType) { + // + // const isPseudoElement: boolean = tokens[0].val.startsWith('::'); + // + // if (!mozExtensions.has(tokens[0].val) && !webkitExtensions.has(tokens[0].val) && !(tokens[0].val in config.selectors) && !(!isPseudoElement &&( ':' + tokens[0].val) in config.selectors)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'unknown pseudo-class: ' + tokens[0].val, + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // else if ([ + // EnumToken.ClassSelectorTokenType, + // EnumToken.HashTokenType, + // EnumToken.PseudoClassFuncTokenType].includes(tokens[0].typ)) { + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { + // + // if (!options?.nestedSelector) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'nested selector not allowed', + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // // validate namespace + // if (tokens[0].typ == EnumToken.NameSpaceAttributeTokenType) { + // + // if (!((tokens[0].l == null || tokens[0].l.typ == EnumToken.IdenTokenType || (tokens[0].l.typ == EnumToken.LiteralTokenType && tokens[0].l.val == '*')) && + // tokens[0].r.typ == EnumToken.IdenTokenType)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'expecting wq-name', + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue; + // } + // // validate attribute + // else if (tokens[0].typ == EnumToken.AttrTokenType) { + // + // const children: Token[] = tokens[0].chi.slice() as Token[]; + // + // consumeWhitespace(children); + // + // if (children.length == 0) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'invalid attribute selector', + // tokens + // } + // } + // + // if (![ + // EnumToken.IdenTokenType, + // EnumToken.NameSpaceAttributeTokenType, + // EnumToken.MatchExpressionTokenType].includes(children[0].typ)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'invalid attribute selector', + // tokens + // } + // } + // + // if (children[0].typ == EnumToken.MatchExpressionTokenType) { + // + // if (![EnumToken.IdenTokenType, + // EnumToken.NameSpaceAttributeTokenType].includes(children[0].l.typ) || + // ![ + // EnumToken.EqualMatchTokenType, EnumToken.DashMatchTokenType, + // EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, + // EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType].includes(children[0].op.typ) || + // ![EnumToken.StringTokenType, + // EnumToken.IdenTokenType].includes(children[0].r.typ)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'invalid attribute selector', + // tokens + // } + // } + // + // if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'invalid attribute selector', + // tokens + // } + // } + // } + // + // children.shift(); + // consumeWhitespace(children); + // + // if (children.length > 0) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: children[0], + // syntax: null, + // error: 'unexpected token', + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue; + // } + // + // break; + // } + // + // if (tokens.length == 0) { + // + // break + // } + // + // // combinator + // if (!combinatorsTokens.includes(tokens[0].typ)) { + // + // if (tokens[0].typ == EnumToken.NestingSelectorTokenType) { + // + // if (!options?.nestedSelector) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'nested selector not allowed', + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // if (tokens[0].typ == EnumToken.PseudoClassTokenType) { + // + // const isPseudoElement: boolean = tokens[0].val.startsWith('::'); + // + // if (!(tokens[0].val in config.selectors) && (isPseudoElement && !(!isPseudoElement &&( ':' + tokens[0].val) in config.selectors))) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // // @ts-ignore + // node: tokens[0], + // syntax: null, + // error: 'unknown pseudo-class', + // tokens + // } + // } + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // + // if (tokens.length > 0 && + // [ + // EnumToken.IdenTokenType, + // EnumToken.AttrTokenType, + // EnumToken.NameSpaceAttributeTokenType, + // EnumToken.ClassSelectorTokenType, + // EnumToken.HashTokenType, + // EnumToken.UniversalSelectorTokenType, + // EnumToken.PseudoClassFuncTokenType, + // EnumToken.PseudoClassFuncTokenType].includes(tokens[0].typ)) { + // + // tokens.shift(); + // consumeWhitespace(tokens); + // continue + // } + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: tokens[0], + // syntax: null, + // error: 'expecting combinator or subclass-selector: ' + JSON.stringify(tokens[0]), + // tokens + // } + // } + // + // const token = tokens.shift() as Token; + // consumeWhitespace(tokens); + // + // if (tokens.length == 0) { + // + // // @ts-ignore + // return { + // valid: ValidationLevel.Drop, + // matches: [], + // node: token, + // syntax: null, + // error: 'expected compound-selector', + // tokens + // } + // } } // @ts-ignore - return { - valid: ValidationLevel.Valid, + return result ?? { + valid: ValidationLevel.Drop, matches: [], - node: null, + node: root, syntax: null, - error: '', + error: 'expecting compound-selector', tokens } } \ No newline at end of file diff --git a/src/lib/validation/syntaxes/compound-selector.ts b/src/lib/validation/syntaxes/compound-selector.ts new file mode 100644 index 00000000..3ae4f3eb --- /dev/null +++ b/src/lib/validation/syntaxes/compound-selector.ts @@ -0,0 +1,262 @@ +import type {AstAtRule, AstRule, Token} from "../../../@types"; +import type { + ValidationConfiguration, + ValidationSelectorOptions, + ValidationSyntaxResult +} from "../../../@types/validation"; +import {EnumToken, ValidationLevel} from "../../ast"; +import {consumeWhitespace} from "../utils"; +import {mozExtensions, webkitExtensions} from "../../syntax"; +import {getSyntaxConfig} from "../config"; + +export function validateCompoundSelector(tokens: Token[], root?: AstAtRule | AstRule, options?: ValidationSelectorOptions): ValidationSyntaxResult { + + if (tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected selector', + tokens + } + } + + tokens = tokens.slice(); + consumeWhitespace(tokens); + + const config: ValidationConfiguration = getSyntaxConfig(); + + let match: number = 0; + let length: number = tokens.length; + + while (tokens.length > 0) { + + while (tokens.length > 0 && tokens[0].typ == EnumToken.NestingSelectorTokenType) { + + if (!options?.nestedSelector) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'nested selector not allowed', + tokens + } + } + + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + + // + while (tokens.length > 0 && + [ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType, + EnumToken.ClassSelectorTokenType, + EnumToken.HashTokenType, + EnumToken.UniversalSelectorTokenType + ].includes(tokens[0].typ)) { + + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + + while (tokens.length > 0 && tokens[0].typ == EnumToken.PseudoClassFuncTokenType) { + + if ( + !mozExtensions.has(tokens[0].val + '()') && + !webkitExtensions.has(tokens[0].val + '()') && + !((tokens[0].val + '()') in config.selectors) + ) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val + '()', + tokens + } + } + + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + + while (tokens.length > 0 && tokens[0].typ == EnumToken.PseudoClassTokenType) { + + const isPseudoElement: boolean = tokens[0].val.startsWith('::'); + + if ( + // https://developer.mozilla.org/en-US/docs/Web/CSS/WebKit_Extensions#pseudo-elements + !(isPseudoElement && tokens[0].val.startsWith('::-webkit-')) && + !mozExtensions.has(tokens[0].val) && + !webkitExtensions.has(tokens[0].val) && + !(tokens[0].val in config.selectors) && + !(!isPseudoElement && + (':' + tokens[0].val) in config.selectors) + ) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + syntax: null, + error: 'unknown pseudo-class: ' + tokens[0].val, + tokens + } + } + + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + + while (tokens.length > 0 && tokens[0].typ == EnumToken.AttrTokenType) { + + const children: Token[] = tokens[0].chi.slice() as Token[]; + + consumeWhitespace(children); + + if (children.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + } + } + + if (![ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType, + EnumToken.MatchExpressionTokenType + ].includes(children[0].typ)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + } + } + + if (children[0].typ == EnumToken.MatchExpressionTokenType) { + + if (children.length != 1) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid ', + tokens + } + } + + if (![ + EnumToken.IdenTokenType, + EnumToken.NameSpaceAttributeTokenType + ].includes(children[0].l.typ) || + ![ + EnumToken.EqualMatchTokenType, EnumToken.DashMatchTokenType, + EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, + EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType].includes(children[0].op.typ) || + ![ + EnumToken.StringTokenType, + EnumToken.IdenTokenType + ].includes(children[0].r.typ)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + } + } + + if (children[0].attr != null && !['i', 's'].includes(children[0].attr)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: null, + error: 'invalid attribute selector', + tokens + } + } + } + + match++; + tokens.shift(); + consumeWhitespace(tokens); + } + + if (length == tokens.length) { + + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: tokens[0], + // @ts-ignore + syntax: null, + error: 'expected compound selector', + tokens + } + } + + length = tokens.length; + } + + return match == 0 ? { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expected compound selector', + tokens + } as ValidationSyntaxResult : + // @ts-ignore + { + valid: ValidationLevel.Valid, + matches: [] as Token[], + // @ts-ignore + node: root as Token, + // @ts-ignore + syntax: null, + error: null, + tokens + } as ValidationSyntaxResult +} diff --git a/src/lib/validation/syntaxes/relative-selector-list.ts b/src/lib/validation/syntaxes/relative-selector-list.ts index 596cb4d2..e00122af 100644 --- a/src/lib/validation/syntaxes/relative-selector-list.ts +++ b/src/lib/validation/syntaxes/relative-selector-list.ts @@ -1,29 +1,64 @@ import type {AstAtRule, AstRule, Token} from "../../../@types"; import type {ValidationSelectorOptions, ValidationSyntaxResult} from "../../../@types/validation.d.ts"; -import {EnumToken, ValidationLevel} from "../../ast"; +import {ValidationLevel} from "../../ast"; import {validateRelativeSelector} from "./relative-selector"; +import {consumeWhitespace, splitTokenList} from "../utils"; export function validateRelativeSelectorList(tokens: Token[], root?: AstAtRule | AstRule, options?: ValidationSelectorOptions): ValidationSyntaxResult { - let i: number = 0; + let i: number = -1; let j: number = 0; let result: ValidationSyntaxResult | null = null; - while (i + 1 < tokens.length) { + tokens = tokens.slice(); - if (tokens[++i].typ == EnumToken.CommaTokenType) { + consumeWhitespace(tokens); - result = validateRelativeSelector(tokens.slice(j, i), root, options); + if (tokens.length == 0) { - if (result.valid == ValidationLevel.Drop) { + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'expecting relative selector list', + tokens + } + } + + for (const t of splitTokenList(tokens)) { + + if (t.length == 0) { - return result; + return { + valid: ValidationLevel.Drop, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: 'unexpected comma', + tokens } + } + + const result: ValidationSyntaxResult = validateRelativeSelector(t, root, options); - j = i + 1; - i = j; + if (result.valid == ValidationLevel.Drop) { + return result; } } - return validateRelativeSelector(i == j ? tokens.slice(i) : tokens.slice(j, i + 1), root, options); + return { + valid: ValidationLevel.Valid, + matches: [], + // @ts-ignore + node: root, + // @ts-ignore + syntax: null, + error: '', + tokens + } } \ No newline at end of file diff --git a/src/lib/validation/utils/list.ts b/src/lib/validation/utils/list.ts index 9885c875..578f6faa 100644 --- a/src/lib/validation/utils/list.ts +++ b/src/lib/validation/utils/list.ts @@ -1,16 +1,16 @@ import {EnumToken} from "../../ast"; import type {Token} from "../../../@types"; -export function splitTokenList(tokenList: Token[]): Token[][] { +export function splitTokenList(tokenList: Token[], split: EnumToken[] = [EnumToken.CommaTokenType]): Token[][] { - return tokenList.reduce( (acc: Token[][], curr: Token): Token[][] => { + return tokenList.reduce((acc: Token[][], curr: Token): Token[][] => { if (curr.typ == EnumToken.CommentTokenType) { return acc; } - if (curr.typ == EnumToken.CommaTokenType) { + if (split.includes(curr.typ)) { acc.push([]); } else { diff --git a/test/specs/code/at-rules.js b/test/specs/code/at-rules.js new file mode 100644 index 00000000..65dd1e70 --- /dev/null +++ b/test/specs/code/at-rules.js @@ -0,0 +1,371 @@ +export function run(describe, expect, transform, parse, render, dirname) { + + describe('media queries level 5', function () { + it('empty query #1', function () { + return transform(` +@media { + + p .a { color: red; } + p .b { color: red; } + } +} + +`).then((result) => expect(result.code).equals(`p .a,p .b{color:red}`)); + }); + + it('error handling #2', function () { + return transform(` + @media &test, all, (example, all,), speech { + + p .a { color: red; } + p .b { color: red; } + } + } + +`).then((result) => expect(result.code).equals(`@media speech{p .a,p .b{color:red}}`)); + }); + + it('error handling #3', function () { + return transform(` + @media (hover) and (width > 1024px) or (--modern), (color) { + .a { + color: green; } + + } +`).then((result) => expect(result.code).equals(`@media (color){.a{color:green}}`)); + }); + + it('error handling #4', function () { + return transform(` + @media (hover) and ((width > 1024px) and (--modern) and (color)), (color) { + .a { + color: green; } + + } +`).then((result) => expect(result.code).equals(`@media (hover) and ((width>1024px) and (--modern) and (color)),(color){.a{color:green}}`)); + }); + + it('error handling #5', function () { + return transform(` + @media (hover) and ((width > 1024px) and (--modern) or (color)), (color) { + .a { + color: green; } + + } +`).then((result) => expect(result.code).equals(`@media (color){.a{color:green}}`)); + }); + + it('error handling #6', function () { + return transform(` +@when media(width >= 400px) and media(pointer: fine) and supports(display: flex) { + +.a { + color: green; } +} +@else { + +.a { + color: red; } +} +@else supports(caret-color: pink) and supports(background: double-rainbow()){ + +.a { + color: green; } +} +`, {beautify: true}).then((result) => expect(result.code).equals(`@when media(width>=400px) and media(pointer:fine) and supports(display:flex) { + .a { + color: green + } +} +@else { + .a { + color: red + } +}`)); + }); + + it('support font-tech, font-format #7', function () { + return transform(` + +@when font-tech(color-COLRv1) and font-tech(variations) { + @font-face { font-family: icons; src: url(icons-gradient-var.woff2); } +} +@else font-tech(color-SVG) { + @font-face { font-family: icons; src: url(icons-gradient.woff2); } +} +@else font-tech(color-COLRv0) { + @font-face { font-family: icons; src: url(icons-flat.woff2); } +} +@else { + @font-face { font-family: icons; src: url(icons-fallback.woff2); } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@when font-tech(color-COLRv1) and font-tech(variations) { + @font-face { + font-family: icons; + src: url(icons-gradient-var.woff2) + } +} +@else font-tech(color-SVG) { + @font-face { + font-family: icons; + src: url(icons-gradient.woff2) + } +} +@else font-tech(color-COLRv0) { + @font-face { + font-family: icons; + src: url(icons-flat.woff2) + } +} +@else { + @font-face { + font-family: icons; + src: url(icons-fallback.woff2) + } +}`)); + }); + + it('font-face #8', function () { + + return transform(` + +@font-face { font-family: icons; src: url(icons-fallback.woff2); +@supports font-tech(color-COLRv1) { + @font-face { font-family: icons; src: url(icons-gradient-var.woff2); } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@font-face { + font-family: icons; + src: url(icons-fallback.woff2); + @supports font-tech(color-COLRv1) { + @font-face { + font-family: icons; + src: url(icons-gradient-var.woff2) + } + } +}`)); + }); + + it('container #9', function () { + + return transform(` + +/* condition list */ +@container card scroll-state(stuck: top) and + style(--themeBackground), + not style(background-color: red), + style(color: green) and style(background-color: transparent), + style(--themeColor: blue) or style(--themeColor: purple){ + h2 { + font-size: 1.5em; + } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@container card scroll-state(stuck:top) and style(--themeBackground),not style(background-color:red),style(color:green) and style(background-color:transparent),style(--themeColor:blue) or style(--themeColor:purple) { + h2 { + font-size: 1.5em + } +}`)); + }); + + it('container #10', function () { + + return transform(` + +/* condition list */ +@container { + h2 { + font-size: 1.5em; + } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(``)); + }); + + it('container #11', function () { + + return transform(` + +/* condition list */ +@container card { + h2 { + font-size: 1.5em; + } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@container card { + h2 { + font-size: 1.5em + } +}`)); + }); + + it('container #12', function () { + + return transform(` + +/* condition list */ +@container card card{ + h2 { + font-size: 1.5em; + } +} +@container card style() { + h2 { + font-size: 1.5em; + } +} +@container card (()) { + h2 { + font-size: 1.5em; + } +} + +@container card ((--themeBackground) and (--themeColor),) { + h2 { + font-size: 1.5em; + } +} + +@container card ((--themeBackground) not (--themeColor)) { + h2 { + font-size: 1.5em; + } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(``)); + }); + + it('container #13', function () { + + return transform(` + +/* condition list */ +@container card ; + +`, {beautify: true}).then((result) => expect(result.code).equals(``)); + }); + + it('custom-media #14', function () { + + return transform(` + /* --modern targets modern devices that support color or hover */ +@custom-media --modern (color), (hover); + +@media (--modern) and (width > 1024px) { + .a { color: green; } +} + +`).then((result) => expect(result.code).equals(`@custom-media --modern (color),(hover);@media (--modern) and (width>1024px){.a{color:green}}`)); + }); + + it('when-else #15', function () { + + return transform(` +@when media(width >= 400px) and media(pointer: fine) and supports(display: flex) { + +.a { + color: green; } +} +@else supports(caret-color: pink) and supports(background: double-rainbow()) { + +.a { + color: green; } +} +@else { + +.a { + color: green; } +} +`, {beautify: true}).then((result) => expect(result.code).equals(`@when media(width>=400px) and media(pointer:fine) and supports(display:flex) { + .a { + color: green + } +} +@else supports(caret-color:pink) and supports(background:double-rainbow()) { + .a { + color: green + } +} +@else { + .a { + color: green + } +}`)); + }); + + it('counter-syle #16', function () { + + return transform(` + +/* condition list */@counter-style thumbs { + system: cyclic; + symbols: "\\1F44D"; + suffix: " "; +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@counter-style thumbs { + system: cyclic; + symbols: "👍"; + suffix: " " +}`)); + }); + + it('counter-style #17', function () { + + return transform(` + +/* condition list */ +@counter-style { + system: cyclic; + symbols: "\\1F44D"; + suffix: " "; +} + +@counter-style thumbs; +@counter-style thumbs thumbs; +@counter-style /* thumbs thumbs */; +@counter-style var(); + +`, {beautify: true}).then((result) => expect(result.code).equals(``)); + }); + + it('at-rule #18', function () { + + return transform(`@charset "UTF-8" +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(``)); + }); + + it('at-rule #19', function () { + + return transform(`@charset "UTF-8" +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(`@charset "UTF-8";`)); + }); + + it('at-rule #20', function () { + + return transform(`@charset 'UTF-8' +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(``)); + }); + + it('at-rule #21', function () { + + return transform(`@charset /* erw */ 'UTF-8'; +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(``)); + }); + + it('at-rule #22', function () { + + return transform(`@charset /* erw */ "UTF-8"; +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(``)); + }); + + it('at-rule #23', function () { + + return transform(`@charset /* erw */"UTF-8"; +`, {beautify: true, removeCharset: false}).then((result) => expect(result.code).equals(`@charset "UTF-8";`)); + }); + }); + +} diff --git a/test/specs/code/block.js b/test/specs/code/block.js index cd59a239..c26cc1a1 100644 --- a/test/specs/code/block.js +++ b/test/specs/code/block.js @@ -708,6 +708,18 @@ content: '\\21 now\\21'; .foo-bar,div#flavor { width: 12px; height: 25% +}`)); + }); + + it('compound selector #34', function () { + const file = ` + +::selection { + color: red; +} +`; + return parse(file, {inlineCssVariables: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`::selection { + color: red }`)); }); } diff --git a/test/specs/code/index.js b/test/specs/code/index.js index eda15fd0..3eb19c0a 100644 --- a/test/specs/code/index.js +++ b/test/specs/code/index.js @@ -16,4 +16,5 @@ export * as sourceMap from './sourcemaps.js'; export * as vars from './vars.js'; export * as visitors from './visitors.js'; export * as walk from './walk.js'; -export * as validation from './validation.js'; \ No newline at end of file +export * as validation from './validation.js'; +export * as atRules from './at-rules.js'; \ No newline at end of file diff --git a/test/specs/code/prefix.js b/test/specs/code/prefix.js index 62059ba3..f41e188e 100644 --- a/test/specs/code/prefix.js +++ b/test/specs/code/prefix.js @@ -41,18 +41,17 @@ export function run(describe, expect, transform, parse, render, dirname, readFil @media screen { - .foo:-webkit-autofiller:not(:hover) { + .foo:-webkit-autofill:not(:hover) { height: calc(100px * 2/ 15); } } `, {removePrefix: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`@media screen { - .foo:-webkit-autofiller:not(:hover) { + .foo:autofill:not(:hover) { height: calc(40px/3) } }`)); }); - it('selector invalid prefix #4', function () { return transform(` @@ -65,5 +64,32 @@ export function run(describe, expect, transform, parse, render, dirname, readFil `, {removePrefix: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(``)); }); + it('prefixed properties #4', function () { + return transform(` + +@media screen { + +:root { + + --color: red; + } + .foo:-webkit-any-link { + height: calc(100px * 2/ 15); + -webkit-appearance: none;; + -moz-window-shadow: menu; + } +} +`, {removePrefix: true}).then(result => expect(render(result.ast, {minify: false}).code).equals(`@media screen { + :root { + --color: red + } + .foo:-webkit-any-link { + height: calc(40px/3); + appearance: none; + -moz-window-shadow: menu + } +}`)); + }); + }); } \ No newline at end of file