diff --git a/.editorconfig b/.editorconfig index 04a6282..df05337 100644 --- a/.editorconfig +++ b/.editorconfig @@ -403,6 +403,365 @@ ij_typescript_while_brace_force = never ij_typescript_while_on_new_line = false ij_typescript_wrap_comments = false +[*.svelte] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +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_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_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 = true +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 = true +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 = never +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 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_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_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 +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_function = 1 +ij_typescript_blank_lines_around_method = 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_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_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_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_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 = true +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 = true +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 = never +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 + [{*.htm,*.html,*.sht,*.shtm,*.shtml}] ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 ij_html_align_attributes = true diff --git a/.github/workflows/build-and-tests.yml b/.github/workflows/build-and-tests.yml new file mode 100644 index 0000000..a672dd9 --- /dev/null +++ b/.github/workflows/build-and-tests.yml @@ -0,0 +1,31 @@ +name: Testing + +on: + push: + branches: + - master + pull_request: + branches: + - master + - 'release/**' + +jobs: + run-tests: + name: 'Run Unit Tests' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install npm dependencies + run: npm ci + + - name: Building the extension + run: npm run build + + - name: Running unit tests + run: npm run test diff --git a/.gitignore b/.gitignore index 28409d9..bfbf150 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,11 @@ .DS_Store node_modules /build +/coverage /.svelte-kit /package .env .env.* !.env.example vite.config.js.timestamp-* -vite.config.ts.timestamp-* \ No newline at end of file +vite.config.ts.timestamp-* diff --git a/manifest.json b/manifest.json index dfc9922..209070a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Furbooru Tagging Assistant", "description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.", - "version": "0.4.1", + "version": "0.4.2", "browser_specific_settings": { "gecko": { "id": "furbooru-tagging-assistant@thecore.city" @@ -27,7 +27,7 @@ "*://*.furbooru.org/galleries/*" ], "js": [ - "src/content/listing.js" + "src/content/listing.ts" ], "css": [ "src/styles/content/listing.scss" @@ -38,7 +38,7 @@ "*://*.furbooru.org/*" ], "js": [ - "src/content/header.js" + "src/content/header.ts" ], "css": [ "src/styles/content/header.scss" @@ -59,7 +59,7 @@ "*://*.furbooru.org/filters/*" ], "js": [ - "src/content/tags.js" + "src/content/tags.ts" ] }, { @@ -67,7 +67,7 @@ "*://*.furbooru.org/images/*" ], "js": [ - "src/content/tags-editor.js" + "src/content/tags-editor.ts" ] } ], diff --git a/package-lock.json b/package-lock.json index 39ee1b9..9332b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "furbooru-tagging-assistant", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "furbooru-tagging-assistant", - "version": "0.4.1", + "version": "0.4.2", "dependencies": { "@fortawesome/fontawesome-free": "^6.7.2", "lz-string": "^1.5.0" @@ -17,12 +17,15 @@ "@sveltejs/kit": "^2.17.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@types/chrome": "^0.0.304", + "@vitest/coverage-v8": "^3.0.6", "cheerio": "^1.0.0", + "jsdom": "^26.0.0", "sass": "^1.85.0", "svelte": "^5.20.1", "svelte-check": "^4.1.4", "typescript": "^5.7.3", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vitest": "^3.0.6" } }, "node_modules/@ampproject/remapping": { @@ -38,6 +41,195 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", + "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.1", + "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", + "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", + "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", + "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.1", + "@csstools/css-calc": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.24.2", "cpu": [ @@ -62,6 +254,34 @@ "node": ">=6" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -183,6 +403,17 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "dev": true, @@ -583,6 +814,152 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/coverage-v8": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.6.tgz", + "integrity": "sha512-JRTlR8Bw+4BcmVTICa7tJsxqphAktakiLsAmibVLAWbu1lauFddY/tXeM6sAyl1cgkPuXtpnUgaCPhTdz1Qapg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.0.6", + "vitest": "3.0.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.6.tgz", + "integrity": "sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.6", + "@vitest/utils": "3.0.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.6.tgz", + "integrity": "sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.6.tgz", + "integrity": "sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.6.tgz", + "integrity": "sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.6.tgz", + "integrity": "sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.6.tgz", + "integrity": "sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.6.tgz", + "integrity": "sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.6", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -604,6 +981,42 @@ "acorn": ">=8.9.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -613,6 +1026,23 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -622,11 +1052,28 @@ "node": ">= 0.4" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "dev": true, "license": "ISC" }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "dev": true, @@ -639,6 +1086,57 @@ "node": ">=8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cheerio": { "version": "1.0.0", "dev": true, @@ -703,6 +1201,39 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cookie": { "version": "0.6.0", "dev": true, @@ -711,6 +1242,21 @@ "node": ">= 0.6" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-select": { "version": "5.1.0", "dev": true, @@ -737,6 +1283,34 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssstyle": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", + "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^2.8.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -754,6 +1328,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "dev": true, @@ -762,6 +1353,16 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "dev": true, @@ -830,6 +1431,35 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "dev": true, @@ -853,6 +1483,62 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.24.2", "dev": true, @@ -907,6 +1593,26 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "dev": true, @@ -919,6 +1625,39 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -934,6 +1673,161 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "9.1.0", "dev": true, @@ -952,6 +1846,34 @@ "entities": "^4.5.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "dev": true, @@ -986,6 +1908,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "dev": true, @@ -1007,6 +1939,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1016,6 +1955,124 @@ "@types/estree": "^1.0.6" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/kleur": { "version": "4.1.5", "dev": true, @@ -1029,6 +2086,20 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lz-string": { "version": "1.5.0", "license": "MIT", @@ -1045,6 +2116,44 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/micromatch": { "version": "4.0.8", "dev": true, @@ -1058,6 +2167,55 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mri": { "version": "1.2.0", "dev": true, @@ -1114,12 +2272,28 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parse5": { - "version": "7.1.2", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -1148,6 +2322,50 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "dev": true, @@ -1192,6 +2410,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/readdirp": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", @@ -1244,6 +2472,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/sade": { "version": "1.8.1", "dev": true, @@ -1281,11 +2516,80 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "dev": true, "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.0", "dev": true, @@ -1307,6 +2611,137 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/svelte": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.1.tgz", @@ -1384,6 +2819,92 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.78", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.78.tgz", + "integrity": "sha512-fSgYrW0ITH0SR/CqKMXIruYIPpNu5aDgUp22UhYoSrnUQwc7SBqifEBFNce7AAcygUPBo6a/gbtcguWdmko4RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.78" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.78", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.78.tgz", + "integrity": "sha512-jS0svNsB99jR6AJBmfmEWuKIgz91Haya91Z43PATaeHJ24BkMoNRb/jlaD37VYjb0mYf6gRL/HOnvS1zEnYBiw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -1404,6 +2925,32 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.1.tgz", + "integrity": "sha512-Ek7HndSVkp10hmHP9V4qZO1u+pn1RU5sI0Fw+jCU3lyvuMZcgqsNgc6CmJJZyByK4Vm/qotGRJlfgAX8q+4JiA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -1497,6 +3044,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.6.tgz", + "integrity": "sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitefu": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz", @@ -1511,6 +3081,99 @@ } } }, + "node_modules/vitest": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.6.tgz", + "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.6", + "@vitest/mocker": "3.0.6", + "@vitest/pretty-format": "^3.0.6", + "@vitest/runner": "3.0.6", + "@vitest/snapshot": "3.0.6", + "@vitest/spy": "3.0.6", + "@vitest/utils": "3.0.6", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.6", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.6", + "@vitest/ui": "3.0.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "dev": true, @@ -1530,6 +3193,190 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/package.json b/package.json index a683f59..6f4ff32 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "furbooru-tagging-assistant", - "version": "0.4.1", + "version": "0.4.2", "private": true, "scripts": { "build": "npm run build:popup && npm run build:extension", "build:popup": "vite build", "build:extension": "node build-extension.js", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run --coverage", + "test:watch": "vitest watch --coverage" }, "devDependencies": { "@sveltejs/adapter-auto": "^4.0.0", @@ -15,12 +17,15 @@ "@sveltejs/kit": "^2.17.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@types/chrome": "^0.0.304", + "@vitest/coverage-v8": "^3.0.6", "cheerio": "^1.0.0", + "jsdom": "^26.0.0", "sass": "^1.85.0", "svelte": "^5.20.1", "svelte-check": "^4.1.4", "typescript": "^5.7.3", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vitest": "^3.0.6" }, "type": "module", "dependencies": { diff --git a/src/components/debugging/StorageViewer.svelte b/src/components/debugging/StorageViewer.svelte index fdccae6..1201c5b 100644 --- a/src/components/debugging/StorageViewer.svelte +++ b/src/components/debugging/StorageViewer.svelte @@ -1,93 +1,123 @@ - - Back -
+ Back +

- / {storage} - {#each breadcrumbs as [name, entryPath]} - / {name} - {/each} + / {storage} + {#each breadcrumbs as [name, entryPath]} + / {name} + {/each}

{#if targetObject} - -
- {#each Object.entries(targetObject) as [key, value]} - {#if targetObject[key] && typeof targetObject[key] === 'object'} - - {key}: Object - - {:else} - - {key}: {typeof targetObject[key]} = {targetObject[key]} - - {/if} - {/each} -
+ +
+ {#each Object.entries(targetObject) as [key, _]} + {#if targetObject[key] && typeof targetObject[key] === 'object'} + + {key}: Object + + {:else} + + {key}: {resolveType(targetObject[key])} = {resolveValue(targetObject[key])} + + {/if} + {/each} +
{/if} diff --git a/src/components/features/GroupView.svelte b/src/components/features/GroupView.svelte index 7d912e7..e2a0da4 100644 --- a/src/components/features/GroupView.svelte +++ b/src/components/features/GroupView.svelte @@ -1,59 +1,60 @@ -
- Group Name: -
{group.settings.name}
+ Group Name: +
{group.settings.name}
{#if sortedTagsList.length} -
- Tags: - -
- {#each sortedTagsList as tagName} - {tagName} - {/each} -
-
-
+
+ Tags: + +
+ {#each sortedTagsList as tagName} + {tagName} + {/each} +
+
+
{/if} {#if sortedPrefixes.length} -
- Prefixes: - -
- {#each sortedPrefixes as prefixName} - {prefixName}* - {/each} -
-
-
+
+ Prefixes: + +
+ {#each sortedPrefixes as prefixName} + {prefixName}* + {/each} +
+
+
{/if} diff --git a/src/components/features/ProfileView.svelte b/src/components/features/ProfileView.svelte index 59efe3f..138e8a9 100644 --- a/src/components/features/ProfileView.svelte +++ b/src/components/features/ProfileView.svelte @@ -1,36 +1,41 @@ -
- Profile: -
{profile.settings.name}
+ Profile: +
{profile.settings.name}
- Tags: -
- {#each sortedTagsList as tagName} - {tagName} - {/each} -
+ Tags: +
+ {#each sortedTagsList as tagName} + {tagName} + {/each} +
diff --git a/src/components/layout/Footer.svelte b/src/components/layout/Footer.svelte index d20e91d..b321b9e 100644 --- a/src/components/layout/Footer.svelte +++ b/src/components/layout/Footer.svelte @@ -1,32 +1,32 @@ diff --git a/src/components/layout/Header.svelte b/src/components/layout/Header.svelte index 032366e..ab23562 100644 --- a/src/components/layout/Header.svelte +++ b/src/components/layout/Header.svelte @@ -1,28 +1,28 @@
- Furbooru Tagging Assistant + Furbooru Tagging Assistant
diff --git a/src/components/tags/TagsColorContainer.svelte b/src/components/tags/TagsColorContainer.svelte index 84d1bf4..2a1e9e4 100644 --- a/src/components/tags/TagsColorContainer.svelte +++ b/src/components/tags/TagsColorContainer.svelte @@ -1,62 +1,68 @@ -
- + {@render children?.()}
diff --git a/src/components/tags/TagsEditor.svelte b/src/components/tags/TagsEditor.svelte index 1128683..83a433f 100644 --- a/src/components/tags/TagsEditor.svelte +++ b/src/components/tags/TagsEditor.svelte @@ -1,106 +1,113 @@ -
- {#each uniqueTags.values() as tagName} -
- {tagName} - x -
- {/each} - + {#each uniqueTags.values() as tagName} +
+ {tagName} + x +
+ {/each} +
diff --git a/src/components/ui/forms/CheckboxField.svelte b/src/components/ui/forms/CheckboxField.svelte index fd42593..00661eb 100644 --- a/src/components/ui/forms/CheckboxField.svelte +++ b/src/components/ui/forms/CheckboxField.svelte @@ -1,12 +1,20 @@ - - + - + {@render children?.()} diff --git a/src/components/ui/forms/FormContainer.svelte b/src/components/ui/forms/FormContainer.svelte index 6b26760..01950b8 100644 --- a/src/components/ui/forms/FormContainer.svelte +++ b/src/components/ui/forms/FormContainer.svelte @@ -1,11 +1,21 @@ + +
- + {@render children?.()}
diff --git a/src/components/ui/forms/FormControl.svelte b/src/components/ui/forms/FormControl.svelte index 6935307..5be6e92 100644 --- a/src/components/ui/forms/FormControl.svelte +++ b/src/components/ui/forms/FormControl.svelte @@ -1,27 +1,35 @@ - diff --git a/src/components/ui/forms/SelectField.svelte b/src/components/ui/forms/SelectField.svelte index f1e78a8..b258179 100644 --- a/src/components/ui/forms/SelectField.svelte +++ b/src/components/ui/forms/SelectField.svelte @@ -1,40 +1,45 @@ - - + {#each Object.entries(optionPairs) as [value, label]} + + {/each} diff --git a/src/components/ui/forms/TagCategorySelectField.svelte b/src/components/ui/forms/TagCategorySelectField.svelte index a33c1fe..956bdc9 100644 --- a/src/components/ui/forms/TagCategorySelectField.svelte +++ b/src/components/ui/forms/TagCategorySelectField.svelte @@ -1,80 +1,84 @@ - - + diff --git a/src/components/ui/forms/TextField.svelte b/src/components/ui/forms/TextField.svelte index 2a2f54b..4264132 100644 --- a/src/components/ui/forms/TextField.svelte +++ b/src/components/ui/forms/TextField.svelte @@ -1,18 +1,21 @@ - - + diff --git a/src/components/ui/menu/Menu.svelte b/src/components/ui/menu/Menu.svelte index 13f0583..ffd3ab5 100644 --- a/src/components/ui/menu/Menu.svelte +++ b/src/components/ui/menu/Menu.svelte @@ -1,38 +1,46 @@ + + diff --git a/src/components/ui/menu/MenuCheckboxItem.svelte b/src/components/ui/menu/MenuCheckboxItem.svelte index 651bba6..9591e8a 100644 --- a/src/components/ui/menu/MenuCheckboxItem.svelte +++ b/src/components/ui/menu/MenuCheckboxItem.svelte @@ -1,37 +1,44 @@ - - - + + {@render children?.()} diff --git a/src/components/ui/menu/MenuItem.svelte b/src/components/ui/menu/MenuItem.svelte index 616bfbb..eedcad0 100644 --- a/src/components/ui/menu/MenuItem.svelte +++ b/src/components/ui/menu/MenuItem.svelte @@ -1,41 +1,45 @@ - - - {#if icon} - - {/if} - + + {#if icon} + + {/if} + {@render children?.()} diff --git a/src/components/ui/menu/MenuRadioItem.svelte b/src/components/ui/menu/MenuRadioItem.svelte index 1672400..bb59d20 100644 --- a/src/components/ui/menu/MenuRadioItem.svelte +++ b/src/components/ui/menu/MenuRadioItem.svelte @@ -1,37 +1,44 @@ - - - + + {@render children?.()} diff --git a/src/content/header.js b/src/content/header.ts similarity index 100% rename from src/content/header.js rename to src/content/header.ts diff --git a/src/content/listing.js b/src/content/listing.ts similarity index 100% rename from src/content/listing.js rename to src/content/listing.ts diff --git a/src/content/tags-editor.js b/src/content/tags-editor.ts similarity index 100% rename from src/content/tags-editor.js rename to src/content/tags-editor.ts diff --git a/src/content/tags.js b/src/content/tags.ts similarity index 100% rename from src/content/tags.js rename to src/content/tags.ts diff --git a/src/lib/booru/scraped/ScrapedAPI.js b/src/lib/booru/scraped/ScrapedAPI.ts similarity index 69% rename from src/lib/booru/scraped/ScrapedAPI.js rename to src/lib/booru/scraped/ScrapedAPI.ts index f880eb8..7bcb929 100644 --- a/src/lib/booru/scraped/ScrapedAPI.js +++ b/src/lib/booru/scraped/ScrapedAPI.ts @@ -1,19 +1,25 @@ import PostParser from "$lib/booru/scraped/parsing/PostParser"; +type UpdaterFunction = (tags: Set) => Set; + export default class ScrapedAPI { /** * Update the tags of the image using callback. - * @param {number} imageId ID of the image. - * @param {function(Set): Set} callback Callback to call to change the content. - * @return {Promise|null>} Updated tags and aliases list for updating internal cached state. + * @param imageId ID of the image. + * @param callback Callback to call to change the content. + * @return Updated tags and aliases list for updating internal cached state. */ - async updateImageTags(imageId, callback) { + async updateImageTags(imageId: number, callback: UpdaterFunction): Promise | null> { const postParser = new PostParser(imageId); const formData = await postParser.resolveTagEditorFormData(); + const tagsFieldValue = formData.get(PostParser.tagsInputName); + + if (typeof tagsFieldValue !== 'string') { + throw new Error('Missing tags field!'); + } const tagsList = new Set( - formData - .get(PostParser.tagsInputName) + tagsFieldValue .split(',') .map(tagName => tagName.trim()) ); diff --git a/src/lib/booru/scraped/parsing/PageParser.js b/src/lib/booru/scraped/parsing/PageParser.ts similarity index 57% rename from src/lib/booru/scraped/parsing/PageParser.js rename to src/lib/booru/scraped/parsing/PageParser.ts index 8078f75..49cd9f1 100644 --- a/src/lib/booru/scraped/parsing/PageParser.js +++ b/src/lib/booru/scraped/parsing/PageParser.ts @@ -1,17 +1,12 @@ export default class PageParser { - /** @type {string} */ - #url; - /** @type {DocumentFragment|null} */ - #fragment = null; + readonly #url: string; + #fragment: DocumentFragment | null = null; - constructor(url) { + constructor(url: string) { this.#url = url; } - /** - * @return {Promise} - */ - async resolveFragment() { + async resolveFragment(): Promise { if (this.#fragment) { return this.#fragment; } @@ -34,12 +29,12 @@ export default class PageParser { /** * Create a document fragment from the following response. * - * @param {Response} response Response to create a fragment from. Note, that this response will be used. If you need - * to use the same response somewhere else, then you need to pass a cloned version of the response. + * @param response Response to create a fragment from. Note, that this response will be used. If you need to use the + * same response somewhere else, then you need to pass a cloned version of the response. * - * @return {Promise} Resulting document fragment ready for processing. + * @return Resulting document fragment ready for processing. */ - static async resolveFragmentFromResponse(response) { + static async resolveFragmentFromResponse(response: Response): Promise { const documentFragment = document.createDocumentFragment(); const template = document.createElement('template'); template.innerHTML = await response.text(); diff --git a/src/lib/booru/scraped/parsing/PostParser.js b/src/lib/booru/scraped/parsing/PostParser.ts similarity index 53% rename from src/lib/booru/scraped/parsing/PostParser.js rename to src/lib/booru/scraped/parsing/PostParser.ts index af6d104..43704b8 100644 --- a/src/lib/booru/scraped/parsing/PostParser.js +++ b/src/lib/booru/scraped/parsing/PostParser.ts @@ -2,23 +2,19 @@ import PageParser from "$lib/booru/scraped/parsing/PageParser"; import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils"; export default class PostParser extends PageParser { - /** @type {HTMLFormElement} */ - #tagEditorForm; + #tagEditorForm: HTMLFormElement | null = null; - constructor(imageId) { + constructor(imageId: number) { super(`/images/${imageId}`); } - /** - * @return {Promise} - */ - async resolveTagEditorForm() { + async resolveTagEditorForm(): Promise { if (this.#tagEditorForm) { return this.#tagEditorForm; } const documentFragment = await this.resolveFragment(); - const tagsFormElement = documentFragment.querySelector("#tags-form"); + const tagsFormElement = documentFragment.querySelector("#tags-form"); if (!tagsFormElement) { throw new Error("Failed to find the tag editor form"); @@ -37,10 +33,8 @@ export default class PostParser extends PageParser { /** * Resolve the tags and aliases mapping from the post page. - * - * @return {Promise|null>} */ - async resolveTagsAndAliases() { + async resolveTagsAndAliases(): Promise | null> { return PostParser.resolveTagsAndAliasesFromPost( await this.resolveFragment() ); @@ -49,25 +43,32 @@ export default class PostParser extends PageParser { /** * Resolve the list of tags and aliases from the post content. * - * @param {DocumentFragment} documentFragment Real content to parse the data from. + * @param documentFragment Real content to parse the data from. * - * @return {Map|null} Tags and aliases or null if failed to parse. + * @return Tags and aliases or null if failed to parse. */ - static resolveTagsAndAliasesFromPost(documentFragment) { - const imageShowContainer = documentFragment.querySelector('.image-show-container'); - const tagsForm = documentFragment.querySelector('#tags-form'); + static resolveTagsAndAliasesFromPost(documentFragment: DocumentFragment): Map | null { + const imageShowContainer = documentFragment.querySelector('.image-show-container'); + const tagsForm = documentFragment.querySelector('#tags-form'); if (!imageShowContainer || !tagsForm) { return null; } const tagsFormData = new FormData(tagsForm); + const tagsAndAliasesValue = imageShowContainer.dataset.imageTagAliases; + const tagsValue = tagsFormData.get(this.tagsInputName); - const tagsAndAliasesList = imageShowContainer.dataset.imageTagAliases + if (!tagsAndAliasesValue || !tagsValue || typeof tagsValue !== 'string') { + console.warn('Failed to locate tags & aliases!'); + return null; + } + + const tagsAndAliasesList = tagsAndAliasesValue .split(',') .map(tagName => tagName.trim()); - const actualTagsList = tagsFormData.get(this.tagsInputName) + const actualTagsList = tagsValue .split(',') .map(tagName => tagName.trim()); diff --git a/src/lib/browser/StorageHelper.ts b/src/lib/browser/StorageHelper.ts index 19c4afd..85ec6cc 100644 --- a/src/lib/browser/StorageHelper.ts +++ b/src/lib/browser/StorageHelper.ts @@ -22,7 +22,7 @@ export default class StorageHelper { * @return The JSON object or the default value if the entry does not exist. */ async read(key: string, defaultValue: DefaultType | null = null): Promise { - return (await this.#storageArea.get(key))?.[key] || defaultValue; + return (await this.#storageArea.get(key))?.[key] ?? defaultValue; } /** diff --git a/src/lib/components/FullscreenViewer.js b/src/lib/components/FullscreenViewer.ts similarity index 78% rename from src/lib/components/FullscreenViewer.js rename to src/lib/components/FullscreenViewer.ts index 9449dac..48f3c1c 100644 --- a/src/lib/components/FullscreenViewer.js +++ b/src/lib/components/FullscreenViewer.ts @@ -1,30 +1,22 @@ import { BaseComponent } from "$lib/components/base/BaseComponent"; -import MiscSettings from "$lib/extension/settings/MiscSettings"; +import MiscSettings, { type FullscreenViewerSize } from "$lib/extension/settings/MiscSettings"; +import { emit, on } from "$lib/components/events/comms"; +import { eventSizeLoaded } from "$lib/components/events/fullscreen-viewer-events"; export class FullscreenViewer extends BaseComponent { - /** @type {HTMLVideoElement} */ - #videoElement = document.createElement('video'); - /** @type {HTMLImageElement} */ - #imageElement = document.createElement('img'); - #spinnerElement = document.createElement('i'); - #sizeSelectorElement = document.createElement('select'); - #closeButtonElement = document.createElement('i'); - /** @type {number|null} */ - #touchId = null; - /** @type {number|null} */ - #startX = null; - /** @type {number|null} */ - #startY = null; - /** @type {boolean|null} */ - #isClosingSwipeStarted = null; - #isSizeFetched = false; - /** @type {App.ImageURIs|null} */ - #currentURIs = null; + #videoElement: HTMLVideoElement = document.createElement('video'); + #imageElement: HTMLImageElement = document.createElement('img'); + #spinnerElement: HTMLElement = document.createElement('i'); + #sizeSelectorElement: HTMLSelectElement = document.createElement('select'); + #closeButtonElement: HTMLElement = document.createElement('i'); + #touchId: number | null = null; + #startX: number | null = null; + #startY: number | null = null; + #isClosingSwipeStarted: boolean | null = null; + #isSizeFetched: boolean = false; + #currentURIs: App.ImageURIs | null = null; - /** - * @protected - */ - build() { + protected build() { this.container.classList.add('fullscreen-viewer'); this.container.append( @@ -71,10 +63,7 @@ export class FullscreenViewer extends BaseComponent { this.container.classList.remove('loading'); } - /** - * @param {TouchEvent} event - */ - #onTouchStart(event) { + #onTouchStart(event: TouchEvent) { if (this.#touchId !== null) { return; } @@ -88,14 +77,12 @@ export class FullscreenViewer extends BaseComponent { this.#touchId = firstTouch.identifier; this.#startX = firstTouch.clientX; this.#startY = firstTouch.clientY; + this.container.classList.add(FullscreenViewer.#swipeState); } - /** - * @param {TouchEvent} event - */ - #onTouchEnd(event) { - if (this.#touchId === null) { + #onTouchEnd(event: TouchEvent) { + if (this.#touchId === null || this.#startY === null) { return; } @@ -126,11 +113,8 @@ export class FullscreenViewer extends BaseComponent { }); } - /** - * @param {TouchEvent} event - */ - #onTouchMove(event) { - if (this.#touchId === null) { + #onTouchMove(event: TouchEvent) { + if (this.#touchId === null || this.#startY === null || this.#startX === null) { return; } @@ -179,23 +163,17 @@ export class FullscreenViewer extends BaseComponent { } } - /** - * @param {KeyboardEvent} event - */ - #onDocumentKeyPressed(event) { + #onDocumentKeyPressed(event: KeyboardEvent) { if (event.code === 'Escape' || event.code === 'Esc') { this.#close(); } } - /** - * @param {import("$lib/extension/settings/MiscSettings").FullscreenViewerSize} size - */ - #onSizeResolved(size) { + #onSizeResolved(size: FullscreenViewerSize) { this.#sizeSelectorElement.value = size; this.#isSizeFetched = true; - this.emit('size-loaded'); + emit(this.container, eventSizeLoaded, size); } #watchForSizeSelectionChanges() { @@ -232,7 +210,7 @@ export class FullscreenViewer extends BaseComponent { this.#currentURIs = null; this.container.classList.remove(FullscreenViewer.#shownState); - document.body.style.overflow = null; + document.body.style.removeProperty('overflow'); requestAnimationFrame(() => { this.#videoElement.volume = 0; @@ -241,16 +219,18 @@ export class FullscreenViewer extends BaseComponent { }); } - /** - * @param {App.ImageURIs} imageUris - * @return {Promise} - */ - async #resolveCurrentSelectedSizeUrl(imageUris) { + async #resolveCurrentSelectedSizeUrl(imageUris: App.ImageURIs): Promise { if (!this.#isSizeFetched) { - await new Promise(resolve => this.on('size-loaded', resolve)) + await new Promise( + resolve => on( + this.container, + eventSizeLoaded, + resolve + ), + ); } - let targetSize = this.#sizeSelectorElement.value; + let targetSize: FullscreenViewerSize | string = this.#sizeSelectorElement.value; if (!imageUris.hasOwnProperty(targetSize)) { targetSize = FullscreenViewer.#fallbackSize; @@ -264,13 +244,10 @@ export class FullscreenViewer extends BaseComponent { return null; } - return imageUris[targetSize]; + return imageUris[targetSize as FullscreenViewerSize]; } - /** - * @param {App.ImageURIs} imageUris - */ - async show(imageUris) { + async show(imageUris: App.ImageURIs): Promise { this.#currentURIs = imageUris; const url = await this.#resolveCurrentSelectedSizeUrl(imageUris); @@ -308,11 +285,7 @@ export class FullscreenViewer extends BaseComponent { this.container.append(this.#imageElement); } - /** - * @param {string} url - * @return {boolean} - */ - static #isVideoUrl(url) { + static #isVideoUrl(url: string): boolean { return url.endsWith('.mp4') || url.endsWith('.webm'); } @@ -324,10 +297,7 @@ export class FullscreenViewer extends BaseComponent { static #swipeState = 'swiped'; static #minRequiredDistance = 50; - /** - * @type {Record} - */ - static #previewSizes = { + static #previewSizes: Record = { full: 'Full', large: 'Large', medium: 'Medium', diff --git a/src/lib/components/ImageShowFullscreenButton.js b/src/lib/components/ImageShowFullscreenButton.ts similarity index 70% rename from src/lib/components/ImageShowFullscreenButton.js rename to src/lib/components/ImageShowFullscreenButton.ts index fc00370..01a1eaf 100644 --- a/src/lib/components/ImageShowFullscreenButton.js +++ b/src/lib/components/ImageShowFullscreenButton.ts @@ -2,21 +2,23 @@ import { BaseComponent } from "$lib/components/base/BaseComponent"; import { getComponent } from "$lib/components/base/component-utils"; import MiscSettings from "$lib/extension/settings/MiscSettings"; import { FullscreenViewer } from "$lib/components/FullscreenViewer"; +import type { MediaBoxTools } from "$lib/components/MediaBoxTools"; export class ImageShowFullscreenButton extends BaseComponent { - /** - * @type {import('./MediaBoxTools').MediaBoxTools|null} - */ - #mediaBoxTools= null; - #isFullscreenButtonEnabled = false; + #mediaBoxTools: MediaBoxTools | null = null; + #isFullscreenButtonEnabled: boolean = false; - build() { + protected build() { this.container.innerText = '🔍'; ImageShowFullscreenButton.#miscSettings ??= new MiscSettings(); } - init() { + protected init() { + if (!this.container.parentElement) { + throw new Error('Missing parent element!'); + } + this.#mediaBoxTools = getComponent(this.container.parentElement); if (!this.#mediaBoxTools) { @@ -32,7 +34,7 @@ export class ImageShowFullscreenButton extends BaseComponent { this.#updateFullscreenButtonVisibility(); }) .then(() => { - ImageShowFullscreenButton.#miscSettings.subscribe(settings => { + ImageShowFullscreenButton.#miscSettings?.subscribe(settings => { this.#isFullscreenButtonEnabled = settings.fullscreenViewer ?? true; this.#updateFullscreenButtonVisibility(); }) @@ -45,28 +47,25 @@ export class ImageShowFullscreenButton extends BaseComponent { } #onButtonClicked() { + const imageLinks = this.#mediaBoxTools?.mediaBox.imageLinks; + + if (!imageLinks) { + throw new Error('Failed to resolve image links from media box tools!'); + } + ImageShowFullscreenButton .#resolveViewer() - .show(this.#mediaBoxTools.mediaBox.imageLinks); + ?.show(imageLinks); } - /** - * @type {FullscreenViewer|null} - */ - static #viewer = null; + static #viewer: FullscreenViewer | null = null; - /** - * @return {FullscreenViewer} - */ - static #resolveViewer() { + static #resolveViewer(): FullscreenViewer { this.#viewer ??= this.#buildViewer(); return this.#viewer; } - /** - * @return {FullscreenViewer} - */ - static #buildViewer() { + static #buildViewer(): FullscreenViewer { const element = document.createElement('div'); const viewer = new FullscreenViewer(element); @@ -77,10 +76,7 @@ export class ImageShowFullscreenButton extends BaseComponent { return viewer; } - /** - * @type {MiscSettings|null} - */ - static #miscSettings = null; + static #miscSettings: MiscSettings | null = null; } export function createImageShowFullscreenButton() { diff --git a/src/lib/components/MaintenancePopup.js b/src/lib/components/MaintenancePopup.ts similarity index 82% rename from src/lib/components/MaintenancePopup.js rename to src/lib/components/MaintenancePopup.ts index 3966be1..718c6a8 100644 --- a/src/lib/components/MaintenancePopup.js +++ b/src/lib/components/MaintenancePopup.ts @@ -10,47 +10,27 @@ import { eventMaintenanceStateChanged, eventTagsUpdated } from "$lib/components/events/maintenance-popup-events"; +import type { MediaBoxTools } from "$lib/components/MediaBoxTools"; class BlackListedTagsEncounteredError extends Error { - /** - * @param {string} tagName - */ - constructor(tagName) { - super(`This tag is blacklisted and prevents submission: ${tagName}`); + constructor(tagName: string) { + super(`This tag is blacklisted and prevents submission: ${tagName}`, { + cause: tagName + }); } } export class MaintenancePopup extends BaseComponent { - /** @type {HTMLElement} */ - #tagsListElement = null; - - /** @type {HTMLElement[]} */ - #tagsList = []; - - /** @type {Map} */ - #suggestedInvalidTags = new Map(); - - /** @type {MaintenanceProfile|null} */ - #activeProfile = null; - - /** @type {import('$lib/components/MediaBoxTools').MediaBoxTools} */ - #mediaBoxTools = null; - - /** @type {Set} */ - #tagsToRemove = new Set(); - - /** @type {Set} */ - #tagsToAdd = new Set(); - - /** @type {boolean} */ - #isPlanningToSubmit = false; - - /** @type {boolean} */ - #isSubmitting = false; - - /** @type {number|null} */ - #tagsSubmissionTimer = null; - + #tagsListElement: HTMLElement = document.createElement('div'); + #tagsList: HTMLElement[] = []; + #suggestedInvalidTags: Map = new Map(); + #activeProfile: MaintenanceProfile | null = null; + #mediaBoxTools: MediaBoxTools | null = null; + #tagsToRemove: Set = new Set(); + #tagsToAdd: Set = new Set(); + #isPlanningToSubmit: boolean = false; + #isSubmitting: boolean = false; + #tagsSubmissionTimer: number | null = null; #emitter = emitterAt(this); /** @@ -60,7 +40,6 @@ export class MaintenancePopup extends BaseComponent { this.container.innerHTML = ''; this.container.classList.add('maintenance-popup'); - this.#tagsListElement = document.createElement('div'); this.#tagsListElement.classList.add('tags-list'); this.container.append( @@ -72,14 +51,13 @@ export class MaintenancePopup extends BaseComponent { * @protected */ init() { - const mediaBoxToolsElement = this.container.closest('.media-box-tools'); + const mediaBoxToolsElement = this.container.closest('.media-box-tools'); if (!mediaBoxToolsElement) { throw new Error('Maintenance popup initialized outside of the media box tools!'); } - /** @type {MediaBoxTools|null} */ - const mediaBoxTools = getComponent(mediaBoxToolsElement); + const mediaBoxTools = getComponent(mediaBoxToolsElement); if (!mediaBoxTools) { throw new Error('Media box tools component not found!'); @@ -96,10 +74,7 @@ export class MaintenancePopup extends BaseComponent { mediaBox.on('mouseover', this.#onMouseEnteredArea.bind(this)); } - /** - * @param {MaintenanceProfile|null} activeProfile - */ - #onActiveProfileChanged(activeProfile) { + #onActiveProfileChanged(activeProfile: MaintenanceProfile | null) { this.#activeProfile = activeProfile; this.container.classList.toggle('is-active', activeProfile !== null); this.#refreshTagsList(); @@ -108,8 +83,11 @@ export class MaintenancePopup extends BaseComponent { } #refreshTagsList() { - /** @type {string[]} */ - const activeProfileTagsList = this.#activeProfile?.settings.tags || []; + if (!this.#mediaBoxTools) { + return; + } + + const activeProfileTagsList: string[] = this.#activeProfile?.settings.tags || []; for (const tagElement of this.#tagsList) { tagElement.remove(); @@ -147,17 +125,22 @@ export class MaintenancePopup extends BaseComponent { /** * Detect and process clicks made directly to the tags. - * @param {MouseEvent} event */ - #handleTagClick(event) { - /** @type {HTMLElement} */ - let tagElement = event.target; + #handleTagClick(event: MouseEvent) { + const targetObject = event.target; - if (!tagElement.classList.contains('tag')) { - tagElement = tagElement.closest('.tag'); + + if (!targetObject || !(targetObject instanceof HTMLElement)) { + return; } - if (!tagElement) { + let tagElement: HTMLElement | null = targetObject; + + if (!tagElement.classList.contains('tag')) { + tagElement = tagElement.closest('.tag'); + } + + if (!tagElement?.dataset.name) { return; } @@ -210,7 +193,7 @@ export class MaintenancePopup extends BaseComponent { } async #onSubmissionTimerPassed() { - if (!this.#isPlanningToSubmit || this.#isSubmitting) { + if (!this.#isPlanningToSubmit || this.#isSubmitting || !this.#mediaBoxTools) { return; } @@ -281,6 +264,10 @@ export class MaintenancePopup extends BaseComponent { } #revealInvalidTags() { + if (!this.#mediaBoxTools) { + return; + } + const tagsAndAliases = this.#mediaBoxTools.mediaBox.tagsAndAliases; if (!tagsAndAliases) { @@ -310,18 +297,11 @@ export class MaintenancePopup extends BaseComponent { } } - /** - * @return {boolean} - */ get isActive() { return this.container.classList.contains('is-active'); } - /** - * @param {string} tagName - * @return {HTMLElement} - */ - static #buildTagElement(tagName) { + static #buildTagElement(tagName: string): HTMLElement { const tagElement = document.createElement('span'); tagElement.classList.add('tag'); tagElement.innerText = tagName; @@ -332,28 +312,26 @@ export class MaintenancePopup extends BaseComponent { /** * Marks the tag with red color. - * @param {HTMLElement} tagElement Element to mark. + * @param tagElement Element to mark. */ - static #markTagAsInvalid(tagElement) { + static #markTagAsInvalid(tagElement: HTMLElement) { tagElement.dataset.tagCategory = 'error'; tagElement.setAttribute('data-tag-category', 'error'); } /** * Controller with maintenance settings. - * @type {MaintenanceSettings} */ static #maintenanceSettings = new MaintenanceSettings(); /** * Subscribe to all necessary feeds to watch for every active profile change. Additionally, will execute the callback * at the very start to retrieve the currently active profile. - * @param {function(MaintenanceProfile|null):void} callback Callback to execute whenever selection of active profile - * or profile itself has been changed. - * @return {function(): void} Unsubscribe function. Call it to stop watching for changes. + * @param callback Callback to execute whenever selection of active profile or profile itself has been changed. + * @return Unsubscribe function. Call it to stop watching for changes. */ - static #watchActiveProfile(callback) { - let lastActiveProfileId; + static #watchActiveProfile(callback: (profile: MaintenanceProfile | null) => void): () => void { + let lastActiveProfileId: string | null | undefined = null; const unsubscribeFromProfilesChanges = MaintenanceProfile.subscribe(profiles => { if (lastActiveProfileId) { @@ -393,9 +371,9 @@ export class MaintenancePopup extends BaseComponent { /** * Notify the frontend about new pending submission started. - * @param {boolean} isStarted True if started, false if ended. + * @param isStarted True if started, false if ended. */ - static #notifyAboutPendingSubmission(isStarted) { + static #notifyAboutPendingSubmission(isStarted: boolean) { if (this.#pendingSubmissionCount === null) { this.#pendingSubmissionCount = 0; this.#initializeExitPromptHandler(); @@ -424,9 +402,8 @@ export class MaintenancePopup extends BaseComponent { /** * Amount of pending submissions or NULL if logic was not yet initialized. - * @type {number|null} */ - static #pendingSubmissionCount = null; + static #pendingSubmissionCount: number|null = null; } export function createMaintenancePopup() { diff --git a/src/lib/components/MaintenanceStatusIcon.js b/src/lib/components/MaintenanceStatusIcon.ts similarity index 83% rename from src/lib/components/MaintenanceStatusIcon.js rename to src/lib/components/MaintenanceStatusIcon.ts index 78ebd57..06c5bb6 100644 --- a/src/lib/components/MaintenanceStatusIcon.js +++ b/src/lib/components/MaintenanceStatusIcon.ts @@ -2,16 +2,20 @@ import { BaseComponent } from "$lib/components/base/BaseComponent"; import { getComponent } from "$lib/components/base/component-utils"; import { on } from "$lib/components/events/comms"; import { eventMaintenanceStateChanged } from "$lib/components/events/maintenance-popup-events"; +import type { MediaBoxTools } from "$lib/components/MediaBoxTools"; export class MaintenanceStatusIcon extends BaseComponent { - /** @type {import('./MediaBoxTools').MediaBoxTools} */ - #mediaBoxTools; + #mediaBoxTools: MediaBoxTools | null = null; build() { this.container.innerText = '🔧'; } init() { + if (!this.container.parentElement) { + throw new Error('Missing parent element for the maintenance status icon!'); + } + this.#mediaBoxTools = getComponent(this.container.parentElement); if (!this.#mediaBoxTools) { @@ -21,10 +25,7 @@ export class MaintenanceStatusIcon extends BaseComponent { on(this.#mediaBoxTools, eventMaintenanceStateChanged, this.#onMaintenanceStateChanged.bind(this)); } - /** - * @param {CustomEvent} stateChangeEvent - */ - #onMaintenanceStateChanged(stateChangeEvent) { + #onMaintenanceStateChanged(stateChangeEvent: CustomEvent) { // TODO Replace those with FontAwesome icons later. Those icons can probably be sourced from the website itself. switch (stateChangeEvent.detail) { case "ready": diff --git a/src/lib/components/MediaBoxTools.js b/src/lib/components/MediaBoxTools.ts similarity index 66% rename from src/lib/components/MediaBoxTools.js rename to src/lib/components/MediaBoxTools.ts index f2de6c5..754a424 100644 --- a/src/lib/components/MediaBoxTools.js +++ b/src/lib/components/MediaBoxTools.ts @@ -3,16 +3,15 @@ import { getComponent } from "$lib/components/base/component-utils"; import { MaintenancePopup } from "$lib/components/MaintenancePopup"; import { on } from "$lib/components/events/comms"; import { eventActiveProfileChanged } from "$lib/components/events/maintenance-popup-events"; +import type { MediaBoxWrapper } from "$lib/components/MediaBoxWrapper"; +import type MaintenanceProfile from "$entities/MaintenanceProfile"; export class MediaBoxTools extends BaseComponent { - /** @type {import('./MediaBoxWrapper').MediaBoxWrapper|null} */ - #mediaBox; - - /** @type {MaintenancePopup|null} */ - #maintenancePopup = null; + #mediaBox: MediaBoxWrapper | null = null; + #maintenancePopup: MaintenancePopup | null = null; init() { - const mediaBoxElement = this.container.closest('.media-box'); + const mediaBoxElement = this.container.closest('.media-box'); if (!mediaBoxElement) { throw new Error('Toolbox element initialized outside of the media box!'); @@ -21,6 +20,10 @@ export class MediaBoxTools extends BaseComponent { this.#mediaBox = getComponent(mediaBoxElement); for (let childElement of this.container.children) { + if (!(childElement instanceof HTMLElement)) { + continue; + } + const component = getComponent(childElement); if (!component) { @@ -39,34 +42,25 @@ export class MediaBoxTools extends BaseComponent { on(this, eventActiveProfileChanged, this.#onActiveProfileChanged.bind(this)); } - /** - * @param {CustomEvent} profileChangedEvent - */ - #onActiveProfileChanged(profileChangedEvent) { + #onActiveProfileChanged(profileChangedEvent: CustomEvent) { this.container.classList.toggle('has-active-profile', profileChangedEvent.detail !== null); } - /** - * @return {MaintenancePopup|null} - */ - get maintenancePopup() { + get maintenancePopup(): MaintenancePopup | null { return this.#maintenancePopup; } - /** - * @return {import('./MediaBoxWrapper').MediaBoxWrapper|null} - */ - get mediaBox() { + get mediaBox(): MediaBoxWrapper | null { return this.#mediaBox; } } /** * Create a maintenance popup element. - * @param {HTMLElement[]} childrenElements List of children elements to append to the component. - * @return {HTMLElement} The maintenance popup element. + * @param childrenElements List of children elements to append to the component. + * @return The maintenance popup element. */ -export function createMediaBoxTools(...childrenElements) { +export function createMediaBoxTools(...childrenElements: HTMLElement[]): HTMLElement { const mediaBoxToolsContainer = document.createElement('div'); mediaBoxToolsContainer.classList.add('media-box-tools'); diff --git a/src/lib/components/MediaBoxWrapper.js b/src/lib/components/MediaBoxWrapper.ts similarity index 57% rename from src/lib/components/MediaBoxWrapper.js rename to src/lib/components/MediaBoxWrapper.ts index ff0ec34..9e75ed3 100644 --- a/src/lib/components/MediaBoxWrapper.js +++ b/src/lib/components/MediaBoxWrapper.ts @@ -5,23 +5,18 @@ import { on } from "$lib/components/events/comms"; import { eventTagsUpdated } from "$lib/components/events/maintenance-popup-events"; export class MediaBoxWrapper extends BaseComponent { - #thumbnailContainer = null; - #imageLinkElement = null; - - /** @type {Map|null} */ - #tagsAndAliases = null; + #thumbnailContainer: HTMLElement | null = null; + #imageLinkElement: HTMLAnchorElement | null = null; + #tagsAndAliases: Map | null = null; init() { this.#thumbnailContainer = this.container.querySelector('.image-container'); - this.#imageLinkElement = this.#thumbnailContainer.querySelector('a'); + this.#imageLinkElement = this.#thumbnailContainer?.querySelector('a') || null; on(this, eventTagsUpdated, this.#onTagsUpdatedRefreshTagsAndAliases.bind(this)); } - /** - * @param {CustomEvent|null>} tagsUpdatedEvent - */ - #onTagsUpdatedRefreshTagsAndAliases(tagsUpdatedEvent) { + #onTagsUpdatedRefreshTagsAndAliases(tagsUpdatedEvent: CustomEvent | null>) { const updatedMap = tagsUpdatedEvent.detail; if (!(updatedMap instanceof Map)) { @@ -32,18 +27,13 @@ export class MediaBoxWrapper extends BaseComponent { } #calculateMediaBoxTags() { - /** @type {string[]|string[]} */ - const - tagAliases = this.#thumbnailContainer.dataset.imageTagAliases?.split(', ') || [], - actualTags = this.#imageLinkElement.title.split(' | Tagged: ')[1]?.split(', ') || []; + const tagAliases: string[] = this.#thumbnailContainer?.dataset.imageTagAliases?.split(', ') || []; + const actualTags = this.#imageLinkElement?.title.split(' | Tagged: ')[1]?.split(', ') || []; return buildTagsAndAliasesMap(tagAliases, actualTags); } - /** - * @return {Map|null} - */ - get tagsAndAliases() { + get tagsAndAliases(): Map | null { if (!this.#tagsAndAliases) { this.#tagsAndAliases = this.#calculateMediaBoxTags(); } @@ -51,26 +41,31 @@ export class MediaBoxWrapper extends BaseComponent { return this.#tagsAndAliases; } - get imageId() { - return parseInt( - this.container.dataset.imageId - ); + get imageId(): number { + const imageId = this.container.dataset.imageId; + + if (!imageId) { + throw new Error('Missing image ID'); + } + + return parseInt(imageId); } - /** - * @return {App.ImageURIs} - */ - get imageLinks() { - return JSON.parse(this.#thumbnailContainer.dataset.uris); + get imageLinks(): App.ImageURIs { + const jsonUris = this.#thumbnailContainer?.dataset.uris; + + if (!jsonUris) { + throw new Error('Missing URIs!'); + } + + return JSON.parse(jsonUris); } } /** * Wrap the media box element into the special wrapper. - * @param {HTMLElement} mediaBoxContainer - * @param {HTMLElement[]} childComponentElements */ -export function initializeMediaBox(mediaBoxContainer, childComponentElements) { +export function initializeMediaBox(mediaBoxContainer: HTMLElement, childComponentElements: HTMLElement[]) { new MediaBoxWrapper(mediaBoxContainer) .initialize(); @@ -80,17 +75,12 @@ export function initializeMediaBox(mediaBoxContainer, childComponentElements) { } } -/** - * @param {NodeListOf} mediaBoxesList - */ -export function calculateMediaBoxesPositions(mediaBoxesList) { +export function calculateMediaBoxesPositions(mediaBoxesList: NodeListOf) { window.addEventListener('resize', () => { - /** @type {HTMLElement|null} */ - let lastMediaBox = null, - /** @type {number|null} */ - lastMediaBoxPosition = null; + let lastMediaBox: HTMLElement | null = null; + let lastMediaBoxPosition: number | null = null; - for (let mediaBoxElement of mediaBoxesList) { + for (const mediaBoxElement of mediaBoxesList) { const yPosition = mediaBoxElement.getBoundingClientRect().y; const isOnTheSameLine = yPosition === lastMediaBoxPosition; diff --git a/src/lib/components/SearchWrapper.js b/src/lib/components/SearchWrapper.ts similarity index 75% rename from src/lib/components/SearchWrapper.js rename to src/lib/components/SearchWrapper.ts index 80f2da4..aeebdde 100644 --- a/src/lib/components/SearchWrapper.js +++ b/src/lib/components/SearchWrapper.ts @@ -1,29 +1,25 @@ import { BaseComponent } from "$lib/components/base/BaseComponent"; import { QueryLexer, QuotedTermToken, TermToken, Token } from "$lib/booru/search/QueryLexer"; -import SearchSettings from "$lib/extension/settings/SearchSettings"; +import SearchSettings, { type SuggestionsPosition } from "$lib/extension/settings/SearchSettings"; export class SearchWrapper extends BaseComponent { - /** @type {HTMLInputElement|null} */ - #searchField = null; - /** @type {string|null} */ - #lastParsedSearchValue = null; - /** @type {Token[]} */ - #cachedParsedQuery = []; - #searchSettings = new SearchSettings(); - #arePropertiesSuggestionsEnabled = false; - /** @type {"start"|"end"} */ - #propertiesSuggestionsPosition = "start"; - /** @type {HTMLElement|null} */ - #cachedAutocompleteContainer = null; - /** @type {TermToken|QuotedTermToken|null} */ - #lastTermToken = null; + #searchField: HTMLInputElement | null = null; + #lastParsedSearchValue: string | null = null; + #cachedParsedQuery: Token[] = []; + #searchSettings: SearchSettings = new SearchSettings(); + #arePropertiesSuggestionsEnabled: boolean = false; + #propertiesSuggestionsPosition: SuggestionsPosition = "start"; + #cachedAutocompleteContainer: HTMLElement | null = null; + #lastTermToken: TermToken | QuotedTermToken | null = null; build() { this.#searchField = this.container.querySelector('input[name=q]'); } init() { - this.#searchField.addEventListener('input', this.#onInputFindProperties.bind(this)); + if (this.#searchField) { + this.#searchField.addEventListener('input', this.#onInputFindProperties.bind(this)) + } this.#searchSettings.resolvePropertiesSuggestionsEnabled() .then(isEnabled => this.#arePropertiesSuggestionsEnabled = isEnabled); @@ -31,18 +27,18 @@ export class SearchWrapper extends BaseComponent { .then(position => this.#propertiesSuggestionsPosition = position); this.#searchSettings.subscribe(settings => { - this.#arePropertiesSuggestionsEnabled = settings.suggestProperties; - this.#propertiesSuggestionsPosition = settings.suggestPropertiesPosition; + this.#arePropertiesSuggestionsEnabled = Boolean(settings.suggestProperties); + this.#propertiesSuggestionsPosition = settings.suggestPropertiesPosition || "start"; }); } /** * Catch the user input and execute suggestions logic. - * @param {InputEvent} event Source event to find the input element from. + * @param event Source event to find the input element from. */ - #onInputFindProperties(event) { + #onInputFindProperties(event: Event) { // Ignore events until option is enabled. - if (!this.#arePropertiesSuggestionsEnabled) { + if (!this.#arePropertiesSuggestionsEnabled || !(event.currentTarget instanceof HTMLInputElement)) { return; } @@ -60,20 +56,26 @@ export class SearchWrapper extends BaseComponent { /** * Get the selection position in the search field. - * @return {number} */ - #getInputUserSelection() { + #getInputUserSelection(): number { + if (!this.#searchField) { + throw new Error('Missing search field!'); + } + return Math.min( - this.#searchField.selectionStart, - this.#searchField.selectionEnd + this.#searchField.selectionStart ?? 0, + this.#searchField.selectionEnd ?? 0, ); } /** * Parse the search query and return the list of parsed tokens. Result will be cached for current search query. - * @return {Token[]} */ - #resolveQueryTokens() { + #resolveQueryTokens(): Token[] { + if (!this.#searchField) { + throw new Error('Missing search field!'); + } + const searchValue = this.#searchField.value; if (searchValue === this.#lastParsedSearchValue && this.#cachedParsedQuery) { @@ -88,9 +90,9 @@ export class SearchWrapper extends BaseComponent { /** * Find the currently selected term. - * @return {string|null} Selected term or null if none found. + * @return Selected term or null if none found. */ - #findCurrentTagFragment() { + #findCurrentTagFragment(): string | null { if (!this.#searchField) { return null; } @@ -127,9 +129,9 @@ export class SearchWrapper extends BaseComponent { * * This means, that properties will only be suggested once actual autocomplete logic was activated. * - * @return {HTMLElement|null} Resolved element or nothing. + * @return Resolved element or nothing. */ - #resolveAutocompleteContainer() { + #resolveAutocompleteContainer(): HTMLElement | null { if (this.#cachedAutocompleteContainer) { return this.#cachedAutocompleteContainer; } @@ -141,11 +143,10 @@ export class SearchWrapper extends BaseComponent { /** * Render the list of suggestions into the existing popup or create and populate a new one. - * @param {string[]} suggestions List of suggestion to render the popup from. - * @param {HTMLInputElement} targetInput Target input to attach the popup to. + * @param suggestions List of suggestion to render the popup from. + * @param targetInput Target input to attach the popup to. */ - #renderSuggestions(suggestions, targetInput) { - /** @type {HTMLElement[]} */ + #renderSuggestions(suggestions: string[], targetInput: HTMLInputElement) { const suggestedListItems = suggestions .map(suggestedTerm => this.#renderTermSuggestion(suggestedTerm)); @@ -170,6 +171,10 @@ export class SearchWrapper extends BaseComponent { const listContainer = autocompleteContainer.querySelector('ul'); + if (!listContainer) { + return; + } + switch (this.#propertiesSuggestionsPosition) { case "start": listContainer.prepend(...suggestedListItems); @@ -183,10 +188,11 @@ export class SearchWrapper extends BaseComponent { console.warn("Invalid position for property suggestions!"); } + const parentScrollTop = targetInput.parentElement?.scrollTop ?? 0; autocompleteContainer.style.position = 'absolute'; autocompleteContainer.style.left = `${targetInput.offsetLeft}px`; - autocompleteContainer.style.top = `${targetInput.offsetTop + targetInput.offsetHeight - targetInput.parentElement.scrollTop}px`; + autocompleteContainer.style.top = `${targetInput.offsetTop + targetInput.offsetHeight - parentScrollTop}px`; document.body.append(autocompleteContainer); }) @@ -194,30 +200,28 @@ export class SearchWrapper extends BaseComponent { /** * Loosely estimate where current selected search term is located and return it if found. - * @param {Token[]} tokens Search value to find the actively selected term from. - * @param {number} userSelectionIndex The index of the user selection. - * @return {Token|null} Search term object or NULL if nothing found. + * @param tokens Search value to find the actively selected term from. + * @param userSelectionIndex The index of the user selection. + * @return Search term object or NULL if nothing found. */ - static #findActiveSearchTermPosition(tokens, userSelectionIndex) { + static #findActiveSearchTermPosition(tokens: Token[], userSelectionIndex: number): Token | null { return tokens.find( token => token.index < userSelectionIndex && token.index + token.value.length >= userSelectionIndex - ); + ) ?? null; } /** * Regular expression to search the properties' syntax. - * @type {RegExp} */ static #propertySearchTermHeadingRegExp = /^(?[a-z\d_]+)(?\.(?[a-z]*))?(?:(?.*))?$/; /** * Create a list of suggested elements using the input received from the user. - * @param {string} searchTermValue Original decoded term received from the user. + * @param searchTermValue Original decoded term received from the user. * @return {string[]} List of suggestions. Could be empty. */ - static #resolveSuggestionsFromTerm(searchTermValue) { - /** @type {string[]} */ - const suggestionsList = []; + static #resolveSuggestionsFromTerm(searchTermValue: string): string[] { + const suggestionsList: string[] = []; this.#propertySearchTermHeadingRegExp.lastIndex = 0; const parsedResult = this.#propertySearchTermHeadingRegExp.exec(searchTermValue); @@ -226,22 +230,28 @@ export class SearchWrapper extends BaseComponent { return suggestionsList; } - const propertyName = parsedResult.groups.name; + const propertyName = parsedResult.groups?.name; + + if (!propertyName) { + return suggestionsList; + } + const propertyType = this.#properties.get(propertyName); - const hasOperatorSyntax = Boolean(parsedResult.groups.op_syntax); - const hasValueSyntax = Boolean(parsedResult.groups.value_syntax); + const hasOperatorSyntax = Boolean(parsedResult.groups?.op_syntax); + const hasValueSyntax = Boolean(parsedResult.groups?.value_syntax); // No suggestions for values for now, maybe could add suggestions for namespaces like my:* - if (hasValueSyntax) { + if (hasValueSyntax && propertyType) { if (this.#typeValues.has(propertyType)) { - const givenValue = parsedResult.groups.value; + const givenValue = parsedResult.groups?.value; + const candidateValues = this.#typeValues.get(propertyType) || []; - for (let candidateValue of this.#typeValues.get(propertyType)) { + for (let candidateValue of candidateValues) { if (givenValue && !candidateValue.startsWith(givenValue)) { continue; } - suggestionsList.push(`${propertyName}${parsedResult.groups.op_syntax ?? ''}:${candidateValue}`); + suggestionsList.push(`${propertyName}${parsedResult.groups?.op_syntax ?? ''}:${candidateValue}`); } } @@ -249,11 +259,12 @@ export class SearchWrapper extends BaseComponent { } // If at least one dot placed, start suggesting operators - if (hasOperatorSyntax) { + if (hasOperatorSyntax && propertyType) { if (this.#typeOperators.has(propertyType)) { - const operatorName = parsedResult.groups.op; + const operatorName = parsedResult.groups?.op; + const candidateOperators = this.#typeOperators.get(propertyType) ?? []; - for (let candidateOperator of this.#typeOperators.get(propertyType)) { + for (let candidateOperator of candidateOperators) { if (operatorName && !candidateOperator.startsWith(operatorName)) { continue; } @@ -279,11 +290,10 @@ export class SearchWrapper extends BaseComponent { /** * Render a single suggestion item and connect required events to interact with the user. - * @param {string} suggestedTerm Term to use for suggestion item. - * @return {HTMLElement} Resulting element. + * @param suggestedTerm Term to use for suggestion item. + * @return Resulting element. */ - #renderTermSuggestion(suggestedTerm) { - /** @type {HTMLElement} */ + #renderTermSuggestion(suggestedTerm: string): HTMLElement { const suggestionItem = document.createElement('li'); suggestionItem.classList.add('autocomplete__item', 'autocomplete__item--property'); suggestionItem.dataset.value = suggestedTerm; @@ -311,10 +321,10 @@ export class SearchWrapper extends BaseComponent { /** * Automatically replace the last active token stored in the variable with the new value. - * @param {string} suggestedTerm Term to replace the value with. + * @param suggestedTerm Term to replace the value with. */ - #replaceLastActiveTokenWithSuggestion(suggestedTerm) { - if (!this.#lastTermToken) { + #replaceLastActiveTokenWithSuggestion(suggestedTerm: string) { + if (!this.#lastTermToken || !this.#searchField) { return; } @@ -334,10 +344,10 @@ export class SearchWrapper extends BaseComponent { /** * Find the selected suggestion item(s) and unselect them. Similar to the logic implemented by the Philomena's * front-end. - * @param {HTMLElement} suggestedElement Target element to search from. If element is disconnected from the DOM, - * search will be halted. + * @param suggestedElement Target element to search from. If element is disconnected from the DOM, search will be + * halted. */ - static #findAndResetSelectedSuggestion(suggestedElement) { + static #findAndResetSelectedSuggestion(suggestedElement: HTMLElement) { if (!suggestedElement.parentElement) { return; } diff --git a/src/lib/components/SiteHeaderWrapper.js b/src/lib/components/SiteHeaderWrapper.ts similarity index 68% rename from src/lib/components/SiteHeaderWrapper.js rename to src/lib/components/SiteHeaderWrapper.ts index 3790097..c1b22fe 100644 --- a/src/lib/components/SiteHeaderWrapper.js +++ b/src/lib/components/SiteHeaderWrapper.ts @@ -2,11 +2,10 @@ import { BaseComponent } from "$lib/components/base/BaseComponent"; import { SearchWrapper } from "$lib/components/SearchWrapper"; class SiteHeaderWrapper extends BaseComponent { - /** @type {SearchWrapper|null} */ - #searchWrapper = null; + #searchWrapper: SearchWrapper | null = null; build() { - const searchForm = this.container.querySelector('.header__search'); + const searchForm = this.container.querySelector('.header__search'); this.#searchWrapper = searchForm && new SearchWrapper(searchForm) || null; } @@ -17,7 +16,7 @@ class SiteHeaderWrapper extends BaseComponent { } } -export function initializeSiteHeader(siteHeaderElement) { +export function initializeSiteHeader(siteHeaderElement: HTMLElement) { new SiteHeaderWrapper(siteHeaderElement) .initialize(); } diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.ts similarity index 71% rename from src/lib/components/TagDropdownWrapper.js rename to src/lib/components/TagDropdownWrapper.ts index 3b09f08..cca7caa 100644 --- a/src/lib/components/TagDropdownWrapper.js +++ b/src/lib/components/TagDropdownWrapper.ts @@ -3,45 +3,38 @@ import MaintenanceProfile from "$entities/MaintenanceProfile"; import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings"; import { getComponent } from "$lib/components/base/component-utils"; import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver"; +import { on } from "$lib/components/events/comms"; +import { eventFormEditorUpdated } from "$lib/components/events/tags-form-events"; -const isTagEditorProcessedKey = Symbol(); const categoriesResolver = new CustomCategoriesResolver(); export class TagDropdownWrapper extends BaseComponent { /** * Container with dropdown elements to insert options into. - * @type {HTMLElement} */ - #dropdownContainer; + #dropdownContainer: HTMLElement | null = null; /** * Button to add or remove the current tag into/from the active profile. - * @type {HTMLAnchorElement|null} */ - #toggleOnExistingButton = null; + #toggleOnExistingButton: HTMLAnchorElement | null = null; /** * Button to create a new profile, make it active and add the current tag into the active profile. - * @type {HTMLAnchorElement|null} */ - #addToNewButton = null; + #addToNewButton: HTMLAnchorElement | null = null; /** * Local clone of the currently active profile used for updating the list of tags. - * @type {MaintenanceProfile|null} */ - #activeProfile = null; + #activeProfile: MaintenanceProfile | null = null; /** * Is cursor currently entered the dropdown. - * @type {boolean} */ - #isEntered = false; + #isEntered: boolean = false; - /** - * @type {string|undefined|null} - */ - #originalCategory = null; + #originalCategory: string | undefined | null = null; build() { this.#dropdownContainer = this.container.querySelector('.dropdown__content'); @@ -116,7 +109,7 @@ export class TagDropdownWrapper extends BaseComponent { ); if (!this.#addToNewButton.isConnected) { - this.#dropdownContainer.append(this.#addToNewButton); + this.#dropdownContainer?.append(this.#addToNewButton); } } else { this.#addToNewButton?.remove(); @@ -130,15 +123,16 @@ export class TagDropdownWrapper extends BaseComponent { const profileName = this.#activeProfile.settings.name; let profileSpecificButtonText = `Add to profile "${profileName}"`; + const tagName = this.tagName; - if (this.#activeProfile.settings.tags.includes(this.tagName)) { + if (tagName && this.#activeProfile.settings.tags.includes(tagName)) { profileSpecificButtonText = `Remove from profile "${profileName}"`; } this.#toggleOnExistingButton.innerText = profileSpecificButtonText; if (!this.#toggleOnExistingButton.isConnected) { - this.#dropdownContainer.append(this.#toggleOnExistingButton); + this.#dropdownContainer?.append(this.#toggleOnExistingButton); } return; @@ -148,6 +142,12 @@ export class TagDropdownWrapper extends BaseComponent { } async #onAddToNewClicked() { + const tagName = this.tagName; + + if (!tagName) { + throw new Error('Missing tag name to create the profile!'); + } + const profile = new MaintenanceProfile(crypto.randomUUID(), { name: 'Temporary Profile (' + (new Date().toISOString()) + ')', tags: [this.tagName], @@ -166,6 +166,10 @@ export class TagDropdownWrapper extends BaseComponent { const tagsList = new Set(this.#activeProfile.settings.tags); const targetTagName = this.tagName; + if (!targetTagName) { + throw new Error('Missing tag name!'); + } + if (tagsList.has(targetTagName)) { tagsList.delete(targetTagName); } else { @@ -181,14 +185,14 @@ export class TagDropdownWrapper extends BaseComponent { /** * Watch for changes to active profile. - * @param {(profile: MaintenanceProfile|null) => void} onActiveProfileChange Callback to call when profile was + * @param onActiveProfileChange Callback to call when profile was * changed. */ - static #watchActiveProfile(onActiveProfileChange) { - let lastActiveProfile; + static #watchActiveProfile(onActiveProfileChange: (profile: MaintenanceProfile|null) => void) { + let lastActiveProfile: string | null = null; this.#maintenanceSettings.subscribe((settings) => { - lastActiveProfile = settings.activeProfile; + lastActiveProfile = settings.activeProfile ?? null; this.#maintenanceSettings .resolveActiveProfileAsObject() @@ -199,7 +203,8 @@ export class TagDropdownWrapper extends BaseComponent { const activeProfile = profiles .find(profile => profile.id === lastActiveProfile); - onActiveProfileChange(activeProfile); + onActiveProfileChange(activeProfile ?? null + ); }); this.#maintenanceSettings @@ -212,12 +217,11 @@ export class TagDropdownWrapper extends BaseComponent { /** * Create element for dropdown. - * @param {string} text Base text for the option. - * @param {(event: MouseEvent) => void} onClickHandler Click handler. Event will be prevented by default. - * @return {HTMLAnchorElement} + * @param text Base text for the option. + * @param onClickHandler Click handler. Event will be prevented by default. + * @return */ - static #createDropdownLink(text, onClickHandler) { - /** @type {HTMLAnchorElement} */ + static #createDropdownLink(text: string, onClickHandler: (event: MouseEvent) => void): HTMLAnchorElement { const dropdownLink = document.createElement('a'); dropdownLink.href = '#'; dropdownLink.innerText = text; @@ -232,7 +236,7 @@ export class TagDropdownWrapper extends BaseComponent { } } -export function wrapTagDropdown(element) { +export function wrapTagDropdown(element: HTMLElement) { // Skip initialization when tag component is already wrapped if (getComponent(element)) { return; @@ -244,6 +248,8 @@ export function wrapTagDropdown(element) { categoriesResolver.addElement(tagDropdown); } +const processedElementsSet = new WeakSet(); + export function watchTagDropdownsInTagsEditor() { // We only need to watch for new editor elements if there is a tag editor present on the page if (!document.querySelector('#image_tags_and_source')) { @@ -251,26 +257,35 @@ export function watchTagDropdownsInTagsEditor() { } document.body.addEventListener('mouseover', event => { - /** @type {HTMLElement} */ const targetElement = event.target; - if (targetElement[isTagEditorProcessedKey]) { + if (!(targetElement instanceof HTMLElement)) { return; } - /** @type {HTMLElement|null} */ - const closestTagEditor = targetElement.closest('#image_tags_and_source'); - - if (!closestTagEditor || closestTagEditor[isTagEditorProcessedKey]) { - targetElement[isTagEditorProcessedKey] = true; + if (processedElementsSet.has(targetElement)) { return; } - targetElement[isTagEditorProcessedKey] = true; - closestTagEditor[isTagEditorProcessedKey] = true; + const closestTagEditor = targetElement.closest('#image_tags_and_source'); - for (const tagDropdownElement of closestTagEditor.querySelectorAll('.tag.dropdown')) { + if (!closestTagEditor || processedElementsSet.has(closestTagEditor)) { + processedElementsSet.add(targetElement); + return; + } + + processedElementsSet.add(targetElement); + processedElementsSet.add(closestTagEditor); + + for (const tagDropdownElement of closestTagEditor.querySelectorAll('.tag.dropdown')) { wrapTagDropdown(tagDropdownElement); } - }) + }); + + // When form is submitted, its DOM is completely updated. We need to fetch those tags in this case. + on(document.body, eventFormEditorUpdated, event => { + for (const tagDropdownElement of event.detail.querySelectorAll('.tag.dropdown')) { + wrapTagDropdown(tagDropdownElement); + } + }); } diff --git a/src/lib/components/TagsForm.js b/src/lib/components/TagsForm.js deleted file mode 100644 index 5376ca3..0000000 --- a/src/lib/components/TagsForm.js +++ /dev/null @@ -1,81 +0,0 @@ -import { BaseComponent } from "$lib/components/base/BaseComponent"; -import { getComponent } from "$lib/components/base/component-utils"; - -export class TagsForm extends BaseComponent { - /** - * Collect all the tag categories available on the page and color the tags in the editor according to them. - */ - refreshTagColors() { - const tagCategories = this.#gatherTagCategories(); - const editableTags = this.container.querySelectorAll('.tag'); - - for (let tagElement of editableTags) { - // Tag name is stored in the "remove" link and not in the tag itself. - const removeLink = tagElement.querySelector('a'); - - if (!removeLink) { - continue; - } - - const tagName = removeLink.dataset.tagName; - - if (!tagCategories.has(tagName)) { - continue; - } - - const categoryName = tagCategories.get(tagName); - - tagElement.dataset.tagCategory = categoryName; - tagElement.setAttribute('data-tag-category', categoryName); - } - } - - /** - * Collect list of categories from the tags on the page. - * @return {Map} - */ - #gatherTagCategories() { - /** @type {Map} */ - const tagCategories = new Map(); - - for (let tagElement of document.querySelectorAll('.tag[data-tag-name][data-tag-category]')) { - tagCategories.set(tagElement.dataset.tagName, tagElement.dataset.tagCategory); - } - - return tagCategories; - } - - static watchForEditors() { - document.body.addEventListener('click', event => { - const targetElement = event.target; - - if (!(targetElement instanceof HTMLElement)) { - return; - } - - const tagEditorWrapper = targetElement.closest('#image_tags_and_source'); - - if (!tagEditorWrapper) { - return; - } - - const refreshTrigger = targetElement.closest('.js-taginput-show, #edit-tags') - - if (!refreshTrigger) { - return; - } - - const tagFormElement = tagEditorWrapper.querySelector('#tags-form'); - - /** @type {TagsForm|null} */ - let tagEditor = getComponent(tagFormElement); - - if (!tagEditor || (!tagEditor instanceof TagsForm)) { - tagEditor = new TagsForm(tagFormElement); - tagEditor.initialize(); - } - - tagEditor.refreshTagColors(); - }); - } -} diff --git a/src/lib/components/TagsForm.ts b/src/lib/components/TagsForm.ts new file mode 100644 index 0000000..ed6681d --- /dev/null +++ b/src/lib/components/TagsForm.ts @@ -0,0 +1,150 @@ +import { BaseComponent } from "$lib/components/base/BaseComponent"; +import { getComponent } from "$lib/components/base/component-utils"; +import { emit, on, type UnsubscribeFunction } from "$lib/components/events/comms"; +import { eventFetchComplete } from "$lib/components/events/booru-events"; +import { eventFormEditorUpdated } from "$lib/components/events/tags-form-events"; + +export class TagsForm extends BaseComponent { + protected init() { + // Site sending the event when form is submitted vie Fetch API. We use this event to reload our logic here. + const unsubscribe = on( + this.container, + eventFetchComplete, + () => this.#waitAndDetectUpdatedForm(unsubscribe), + ); + } + + #waitAndDetectUpdatedForm(unsubscribe: UnsubscribeFunction): void { + const elementContainingTagEditor = this.container + .closest('#image_tags_and_source') + ?.parentElement; + + if (!elementContainingTagEditor) { + return; + } + + const observer = new MutationObserver(() => { + const tagsFormElement = elementContainingTagEditor.querySelector('#tags-form'); + + if (!tagsFormElement || getComponent(tagsFormElement)) { + return; + } + + const tagFormComponent = new TagsForm(tagsFormElement); + tagFormComponent.initialize(); + + const fullTagEditor = tagFormComponent.parentTagEditorElement; + + if (fullTagEditor) { + emit(document.body, eventFormEditorUpdated, fullTagEditor); + } else { + console.info('Tag form is not in the tag editor. Event is not sent.'); + } + + observer.disconnect(); + unsubscribe(); + }); + + observer.observe(elementContainingTagEditor, { + subtree: true, + childList: true, + }); + + // Make sure to forcibly disconnect everything after a while. + setTimeout(() => { + observer.disconnect(); + unsubscribe(); + }, 5000); + } + + get parentTagEditorElement(): HTMLElement | null { + return this.container.closest('.js-tagsauce') + } + + /** + * Collect all the tag categories available on the page and color the tags in the editor according to them. + */ + refreshTagColors() { + const tagCategories = this.#gatherTagCategories(); + const editableTags = this.container.querySelectorAll('.tag'); + + for (const tagElement of editableTags) { + // Tag name is stored in the "remove" link and not in the tag itself. + const removeLink = tagElement.querySelector('a'); + + if (!removeLink) { + continue; + } + + const tagName = removeLink.dataset.tagName; + + if (!tagName || !tagCategories.has(tagName)) { + continue; + } + + const categoryName = tagCategories.get(tagName)!; + + tagElement.dataset.tagCategory = categoryName; + tagElement.setAttribute('data-tag-category', categoryName); + } + } + + /** + * Collect list of categories from the tags on the page. + * @return + */ + #gatherTagCategories(): Map { + const tagCategories: Map = new Map(); + + for (const tagElement of document.querySelectorAll('.tag[data-tag-name][data-tag-category]')) { + const tagName = tagElement.dataset.tagName; + const tagCategory = tagElement.dataset.tagCategory; + + if (!tagName || !tagCategory) { + console.warn('Missing tag name or category!'); + continue; + } + + tagCategories.set(tagName, tagCategory); + } + + return tagCategories; + } + + static watchForEditors() { + document.body.addEventListener('click', event => { + const targetElement = event.target; + + if (!(targetElement instanceof HTMLElement)) { + return; + } + + const tagEditorWrapper = targetElement.closest('#image_tags_and_source'); + + if (!tagEditorWrapper) { + return; + } + + const refreshTrigger = targetElement.closest('.js-taginput-show, #edit-tags') + + if (!refreshTrigger) { + return; + } + + const tagFormElement = tagEditorWrapper.querySelector('#tags-form'); + + if (!tagFormElement) { + return; + } + + let tagEditor = getComponent(tagFormElement); + + if (!tagEditor || !(tagEditor instanceof TagsForm)) { + tagEditor = new TagsForm(tagFormElement); + tagEditor.initialize(); + } + + (tagEditor as TagsForm).refreshTagColors(); + }); + } +} diff --git a/src/lib/components/base/BaseComponent.js b/src/lib/components/base/BaseComponent.ts similarity index 52% rename from src/lib/components/base/BaseComponent.js rename to src/lib/components/base/BaseComponent.ts index 04ac8fd..fd3ecdd 100644 --- a/src/lib/components/base/BaseComponent.js +++ b/src/lib/components/base/BaseComponent.ts @@ -1,18 +1,14 @@ import { bindComponent } from "$lib/components/base/component-utils"; -/** - * @abstract - */ -export class BaseComponent { - /** @type {HTMLElement} */ - #container; +type ComponentEventListener = + (this: HTMLElement, event: HTMLElementEventMap[EventName]) => void; + +export class BaseComponent { + readonly #container: ContainerType; #isInitialized = false; - /** - * @param {HTMLElement} container - */ - constructor(container) { + constructor(container: ContainerType) { this.#container = container; bindComponent(container, this); @@ -29,42 +25,33 @@ export class BaseComponent { this.init(); } - /** - * @protected - */ - build() { + protected build(): void { // This method can be implemented by the component classes to modify or create the inner elements. } - /** - * @protected - */ - init() { + protected init(): void { // This method can be implemented by the component classes to initialize the component. - } + }; - /** - * @return {HTMLElement} - */ - get container() { + get container(): ContainerType { return this.#container; } /** * Check if the component is initialized already. If not checked, subsequent calls to the `initialize` method will * throw an error. - * @return {boolean} + * @return */ - get isInitialized() { + get isInitialized(): boolean { return this.#isInitialized; } /** * Emit the custom event on the container element. - * @param {keyof HTMLElementEventMap|string} event The event name. - * @param {any} [detail] The event detail. Can be omitted. + * @param event The event name. + * @param [detail] The event detail. Can be omitted. */ - emit(event, detail = undefined) { + emit(event: keyof HTMLElementEventMap | string, detail: any = undefined): void { this.#container.dispatchEvent( new CustomEvent( event, @@ -78,12 +65,16 @@ export class BaseComponent { /** * Subscribe to the DOM event on the container element. - * @param {keyof HTMLElementEventMap|string} event The event name. - * @param {function(Event): void} listener The event listener. - * @param {AddEventListenerOptions|undefined} [options] The event listener options. Can be omitted. - * @return {function(): void} The unsubscribe function. + * @param event The event name. + * @param listener The event listener. + * @param [options] The event listener options. Can be omitted. + * @return The unsubscribe function. */ - on(event, listener, options = undefined) { + on( + event: EventName, + listener: ComponentEventListener, + options?: AddEventListenerOptions, + ): () => void { this.#container.addEventListener(event, listener, options); return () => void this.#container.removeEventListener(event, listener, options); @@ -91,12 +82,16 @@ export class BaseComponent { /** * Subscribe to the DOM event on the container element. The event listener will be called only once. - * @param {keyof HTMLElementEventMap|string} event The event name. - * @param {function(Event): void} listener The event listener. - * @param {AddEventListenerOptions|undefined} [options] The event listener options. Can be omitted. - * @return {function(): void} The unsubscribe function. + * @param event The event name. + * @param listener The event listener. + * @param [options] The event listener options. Can be omitted. + * @return The unsubscribe function. */ - once(event, listener, options = undefined) { + once( + event: EventName, + listener: ComponentEventListener, + options?: AddEventListenerOptions, + ): () => void { options = options || {}; options.once = true; diff --git a/src/lib/components/base/component-utils.ts b/src/lib/components/base/component-utils.ts index 636b8f4..5f6bd42 100644 --- a/src/lib/components/base/component-utils.ts +++ b/src/lib/components/base/component-utils.ts @@ -2,8 +2,8 @@ import type { BaseComponent } from "$lib/components/base/BaseComponent"; const instanceSymbol = Symbol('instance'); -interface ElementWithComponent extends HTMLElement { - [instanceSymbol]?: BaseComponent; +interface ElementWithComponent extends HTMLElement { + [instanceSymbol]?: T; } /** @@ -11,7 +11,7 @@ interface ElementWithComponent extends HTMLElement { * @param {HTMLElement} element * @return */ -export function getComponent(element: ElementWithComponent): BaseComponent | null { +export function getComponent(element: ElementWithComponent): T | null { return element[instanceSymbol] || null; } @@ -20,7 +20,7 @@ export function getComponent(element: ElementWithComponent): BaseComponent | nul * @param element The element to bind the component to. * @param instance The component instance. */ -export function bindComponent(element: ElementWithComponent, instance: BaseComponent): void { +export function bindComponent(element: ElementWithComponent, instance: T): void { if (element[instanceSymbol]) { throw new Error('The element is already bound to a component.'); } diff --git a/src/lib/components/events/booru-events.ts b/src/lib/components/events/booru-events.ts new file mode 100644 index 0000000..a726d9b --- /dev/null +++ b/src/lib/components/events/booru-events.ts @@ -0,0 +1,5 @@ +export const eventFetchComplete = 'fetchcomplete'; + +export interface BooruEventsMap { + [eventFetchComplete]: null; // Site sends the response, but extension will not get it due to isolation. +} diff --git a/src/lib/components/events/comms.ts b/src/lib/components/events/comms.ts index 8db137c..3624491 100644 --- a/src/lib/components/events/comms.ts +++ b/src/lib/components/events/comms.ts @@ -1,11 +1,17 @@ import type { MaintenancePopupEventsMap } from "$lib/components/events/maintenance-popup-events"; import { BaseComponent } from "$lib/components/base/BaseComponent"; +import type { FullscreenViewerEventsMap } from "$lib/components/events/fullscreen-viewer-events"; +import type { BooruEventsMap } from "$lib/components/events/booru-events"; +import type { TagsFormEventsMap } from "$lib/components/events/tags-form-events"; -interface EventsMapping extends MaintenancePopupEventsMap { -} +type EventsMapping = + MaintenancePopupEventsMap + & FullscreenViewerEventsMap + & BooruEventsMap + & TagsFormEventsMap; type EventCallback = (event: CustomEvent) => void; -type UnsubscribeFunction = () => void; +export type UnsubscribeFunction = () => void; type ResolvableTarget = EventTarget | BaseComponent; function resolveTarget(componentOrElement: ResolvableTarget): EventTarget { diff --git a/src/lib/components/events/fullscreen-viewer-events.ts b/src/lib/components/events/fullscreen-viewer-events.ts new file mode 100644 index 0000000..333a917 --- /dev/null +++ b/src/lib/components/events/fullscreen-viewer-events.ts @@ -0,0 +1,7 @@ +import type { FullscreenViewerSize } from "$lib/extension/settings/MiscSettings"; + +export const eventSizeLoaded = 'size-loaded'; + +export interface FullscreenViewerEventsMap { + [eventSizeLoaded]: FullscreenViewerSize; +} diff --git a/src/lib/components/events/tags-form-events.ts b/src/lib/components/events/tags-form-events.ts new file mode 100644 index 0000000..64461dc --- /dev/null +++ b/src/lib/components/events/tags-form-events.ts @@ -0,0 +1,5 @@ +export const eventFormEditorUpdated = 'tags-form-updated'; + +export interface TagsFormEventsMap { + [eventFormEditorUpdated]: HTMLElement; +} diff --git a/src/lib/extension/settings/MiscSettings.ts b/src/lib/extension/settings/MiscSettings.ts index 0544c29..c8b4508 100644 --- a/src/lib/extension/settings/MiscSettings.ts +++ b/src/lib/extension/settings/MiscSettings.ts @@ -1,6 +1,6 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings"; -export type FullscreenViewerSize = 'small' | 'medium' | 'large' | 'full'; +export type FullscreenViewerSize = keyof App.ImageURIs; interface MiscSettingsFields { fullscreenViewer: boolean; diff --git a/src/lib/extension/settings/SearchSettings.ts b/src/lib/extension/settings/SearchSettings.ts index d0d4f99..373c613 100644 --- a/src/lib/extension/settings/SearchSettings.ts +++ b/src/lib/extension/settings/SearchSettings.ts @@ -1,8 +1,10 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings"; +export type SuggestionsPosition = "start" | "end"; + interface SearchSettingsFields { suggestProperties: boolean; - suggestPropertiesPosition: "start" | "end"; + suggestPropertiesPosition: SuggestionsPosition; } export default class SearchSettings extends CacheableSettings { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4ae27b7..9d1f851 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,21 +1,27 @@ -
- + {@render children?.()}